├── _config.yml ├── example ├── android │ ├── gradle.properties │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── app │ │ │ │ │ └── MainActivity.java │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── .gitignore │ ├── .project │ ├── settings.gradle │ ├── build.gradle │ ├── gradlew.bat │ └── gradlew ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── AppDelegate.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── main.m │ │ ├── AppDelegate.m │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── WorkspaceSettings.xcsettings │ ├── .gitignore │ └── Podfile ├── README.md ├── .gitignore ├── lib │ ├── mutations │ │ └── addStar.dart │ ├── queries │ │ └── readRepositories.dart │ └── main.dart ├── .metadata ├── .idea │ ├── runConfigurations │ │ └── main_dart.xml │ └── modules.xml ├── pubspec.yaml ├── test │ └── widget_test.dart ├── app.iml ├── app_android.iml └── analysis_options.yaml ├── lib ├── src │ ├── utilities │ │ ├── helpers.dart │ │ ├── get_from_ast.dart │ │ └── traverse.dart │ ├── link │ │ ├── web_socket │ │ │ └── link_web_socket.dart │ │ ├── fetch_result.dart │ │ ├── http │ │ │ ├── fallback_http_config.dart │ │ │ ├── http_config.dart │ │ │ └── link_http.dart │ │ ├── link.dart │ │ ├── operation.dart │ │ └── auth │ │ │ └── auth_link.dart │ ├── cache │ │ ├── cache.dart │ │ ├── normalized │ │ │ ├── record_field_json_adapter.dart │ │ │ └── sql │ │ │ │ ├── sql_helper.dart │ │ │ │ └── sql-normalized-cache.dart │ │ ├── normalized_in_memory.dart │ │ └── in_memory.dart │ ├── core │ │ ├── query_result.dart │ │ ├── graphql_error.dart │ │ ├── observable_query.dart │ │ ├── query_options.dart │ │ └── query_manager.dart │ ├── widgets │ │ ├── graphql_consumer.dart │ │ ├── graphql_provider.dart │ │ ├── cache_provider.dart │ │ ├── subscription.dart │ │ ├── query.dart │ │ └── mutation.dart │ ├── graphql_client.dart │ ├── socket_client.dart │ ├── scheduler │ │ └── scheduler.dart │ └── websocket │ │ ├── socket.dart │ │ └── messages.dart └── flutter_graphql.dart ├── test ├── graphql_flutter_test.dart └── normalized_in_memory_test.dart ├── .idea └── modules.xml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── config.yml └── CODE_OF_CONDUCT.md ├── .gitignore ├── pubspec.yaml ├── LICENSE ├── graphql.iml ├── .all-contributorsrc ├── analysis_options.yaml ├── CHANGELOG.md └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /lib/src/utilities/helpers.dart: -------------------------------------------------------------------------------- 1 | bool notNull(Object any) { 2 | return any != null; 3 | } -------------------------------------------------------------------------------- /example/android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # app 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](https://flutter.io/). 9 | -------------------------------------------------------------------------------- /test/graphql_flutter_test.dart: -------------------------------------------------------------------------------- 1 | // import 'package:test/test.dart'; 2 | 3 | // import 'package:flutter_graphql/flutter_graphql.dart'; 4 | 5 | void main() { 6 | // TODO: write tests 7 | } 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juicycleff/flutter-graphql/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | .DS_Store 3 | .dart_tool/ 4 | .idea/libraries/ 5 | .idea/workspace.xml 6 | .idea/tasks.xml 7 | .idea/usage.statistics.xml 8 | 9 | .packages 10 | .pub/ 11 | 12 | build/ 13 | 14 | .flutter-plugins 15 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/lib/mutations/addStar.dart: -------------------------------------------------------------------------------- 1 | const String addStar = ''' 2 | mutation AddStar(\$starrableId: ID!) { 3 | addStar(input: {starrableId: \$starrableId}) { 4 | starrable { 5 | viewerHasStarred 6 | } 7 | } 8 | } 9 | '''; 10 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-rc-1-all.zip 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Describe the purpose of the pull request. 2 | 3 | ### Breaking changes 4 | 5 | - Broke ... because ... . 6 | 7 | #### Fixes / Enhancements 8 | 9 | - Fixed ... was ... . 10 | - Added ... . 11 | - Updated ... . 12 | 13 | #### Docs 14 | 15 | - Added ... . 16 | - Updated ... . 17 | -------------------------------------------------------------------------------- /lib/src/link/web_socket/link_web_socket.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_graphql/src/link/link.dart'; 2 | 3 | class WebSocketLink extends Link { 4 | RequestHandler subscriptionClient; 5 | 6 | // TODO: implement https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-ws/src/webSocketLink.ts 7 | } 8 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f9bb4289e9fd861d70ae78bcc3a042ef1b35cc9d 8 | channel: beta 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/.idea/runConfigurations/main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /lib/src/cache/cache.dart: -------------------------------------------------------------------------------- 1 | abstract class Cache { 2 | Future remove(String key, bool cascade) async {} 3 | 4 | dynamic read(String key) {} 5 | 6 | void write( 7 | String key, 8 | dynamic value, 9 | ) {} 10 | 11 | void save() {} 12 | 13 | void restore() {} 14 | 15 | void reset() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/lib/queries/readRepositories.dart: -------------------------------------------------------------------------------- 1 | const String readRepositories = ''' 2 | query ReadRepositories(\$nRepositories: Int!) { 3 | viewer { 4 | repositories(last: \$nRepositories) { 5 | nodes { 6 | id 7 | name 8 | viewerHasStarred 9 | } 10 | } 11 | } 12 | } 13 | '''; 14 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app 2 | description: A new Flutter project. 3 | 4 | dependencies: 5 | flutter: 6 | sdk: flutter 7 | cupertino_icons: ^0.1.2 8 | flutter_graphql: 9 | path: .. 10 | 11 | dev_dependencies: 12 | flutter_test: 13 | sdk: flutter 14 | test: ^1.3.0 15 | 16 | flutter: 17 | uses-material-design: true 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .vscode/ 3 | .idea/libraries/ 4 | .idea/workspace.xml 5 | .idea/tasks.xml 6 | .idea/usage.statistics.xml 7 | 8 | .DS_Store 9 | .dart_tool/ 10 | 11 | .packages 12 | .pub/ 13 | pubspec.lock 14 | 15 | build/ 16 | ios/.generated/ 17 | ios/Flutter/Generated.xcconfig 18 | ios/Runner/GeneratedPluginRegistrant.* 19 | .flutter-plugins 20 | -------------------------------------------------------------------------------- /lib/src/link/fetch_result.dart: -------------------------------------------------------------------------------- 1 | class FetchResult { 2 | FetchResult({ 3 | this.errors, 4 | this.data, 5 | this.extensions, 6 | this.context, 7 | }); 8 | 9 | List errors; 10 | 11 | /// List or Map 12 | dynamic data; 13 | Map extensions; 14 | Map context; 15 | } -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.app; 2 | 3 | import android.os.Bundle; 4 | 5 | import io.flutter.app.FlutterActivity; 6 | import io.flutter.plugins.GeneratedPluginRegistrant; 7 | 8 | public class MainActivity extends FlutterActivity { 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | GeneratedPluginRegistrant.registerWith(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 7 | [GeneratedPluginRegistrant registerWithRegistry:self]; 8 | // Override point for customization after application launch. 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /example/android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | // import 'package:flutter/material.dart'; 8 | // import 'package:flutter_test/flutter_test.dart'; 9 | 10 | void main() { 11 | // TODO: write tests 12 | } 13 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.1.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_graphql 2 | description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. 3 | version: 1.0.0-rc.3 4 | authors: 5 | - Rex Raphael 6 | homepage: https://github.com/juicycleff/graphql-flutter/tree/master 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | meta: ^1.1.6 12 | http: ^0.12.0 13 | http_parser: ^3.1.3 14 | path_provider: ^0.4.1 15 | uuid: ^2.0.0 16 | sqflite: ^1.1.0 17 | graphql_parser: ^1.1.1 18 | 19 | dev_dependencies: 20 | flutter_test: 21 | sdk: flutter 22 | test: ^1.3.0 23 | 24 | environment: 25 | sdk: ">=2.0.0 <3.0.0" 26 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/app.flx 37 | /Flutter/app.zip 38 | /Flutter/flutter_assets/ 39 | /Flutter/App.framework 40 | /Flutter/Flutter.framework 41 | /Flutter/Generated.xcconfig 42 | /ServiceDefinitions.json 43 | 44 | Pods/ 45 | .symlinks/ 46 | -------------------------------------------------------------------------------- /lib/src/core/query_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_graphql/src/core/graphql_error.dart'; 2 | 3 | class QueryResult { 4 | QueryResult({ 5 | this.data, 6 | this.errors, 7 | this.loading, 8 | this.stale, 9 | }); 10 | 11 | /// List or Map 12 | dynamic data; 13 | List errors; 14 | bool loading; 15 | bool stale; 16 | 17 | bool get hasErrors { 18 | if (errors == null) { 19 | return false; 20 | } 21 | 22 | return errors.isNotEmpty; 23 | } 24 | 25 | void addError(GraphQLError graphQLError) { 26 | if (errors != null) { 27 | errors.add(graphQLError); 28 | } else { 29 | errors = [graphQLError]; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /lib/src/link/http/fallback_http_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_graphql/src/link/http/http_config.dart'; 2 | 3 | HttpQueryOptions defaultHttpOptions = HttpQueryOptions( 4 | includeQuery: true, 5 | includeExtensions: false, 6 | ); 7 | 8 | Map defaultOptions = { 9 | 'method': 'POST', 10 | }; 11 | 12 | Map defaultHeaders = { 13 | 'accept': '*/*', 14 | 'content-type': 'application/json', 15 | }; 16 | 17 | Map defaultCredentials = {}; 18 | 19 | HttpConfig fallbackHttpConfig = HttpConfig( 20 | http: defaultHttpOptions, 21 | options: defaultOptions, 22 | headers: defaultHeaders, 23 | credentials: defaultCredentials, 24 | ); 25 | -------------------------------------------------------------------------------- /lib/src/widgets/graphql_consumer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'package:flutter_graphql/src/graphql_client.dart'; 4 | import 'package:flutter_graphql/src/widgets/graphql_provider.dart'; 5 | 6 | typedef Widget GraphQLConsumerBuilder(GraphQLClient client); 7 | 8 | class GraphQLConsumer extends StatelessWidget { 9 | const GraphQLConsumer({ 10 | final Key key, 11 | @required this.builder, 12 | this.client, 13 | }) : super(key: key); 14 | 15 | final GraphQLConsumerBuilder builder; 16 | final GraphQLClient client; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | GraphQLClient tmpClient; 21 | if (client != null) 22 | tmpClient = client; 23 | else 24 | tmpClient = GraphQLProvider.of(context).value; 25 | assert(tmpClient != null); 26 | 27 | return builder(tmpClient); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/cache/normalized/record_field_json_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:core'; 3 | 4 | class RecordFieldJsonAdapter { 5 | 6 | static RecordFieldJsonAdapter create() { 7 | return new RecordFieldJsonAdapter(); 8 | } 9 | 10 | RecordFieldJsonAdapter() { 11 | } 12 | 13 | dynamic toJson(Map fields) { 14 | assert(fields != null); 15 | return json.encode(fields); 16 | } 17 | 18 | Map from(dynamic jsonObj) { 19 | assert(jsonObj != null); 20 | return json.decode(jsonObj); 21 | } 22 | 23 | /* 24 | private Map fromBufferSource(BufferedSource bufferedFieldSource) throws IOException { 25 | final CacheJsonStreamReader cacheJsonStreamReader = 26 | cacheJsonStreamReader(bufferedSourceJsonReader(bufferedFieldSource)); 27 | return cacheJsonStreamReader.toMap(); 28 | } 29 | */ 30 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Version [e.g. 22] 26 | 27 | **Smartphone (please complete the following information):** 28 | - Device: [e.g. iPhone6] 29 | - OS: [e.g. iOS8.1] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/utilities/get_from_ast.dart: -------------------------------------------------------------------------------- 1 | import 'package:graphql_parser/graphql_parser.dart'; 2 | 3 | String getOperationName(String rawDoc) { 4 | final List tokens = scan(rawDoc); 5 | final Parser parser = Parser(tokens); 6 | 7 | if (parser.errors.isNotEmpty) { 8 | // Handle errors... 9 | print(parser.errors.toString()); 10 | } 11 | 12 | // Parse the GraphQL document using recursive descent 13 | final DocumentContext doc = parser.parseDocument(); 14 | 15 | if (doc.definitions != null && doc.definitions.isNotEmpty) { 16 | final OperationDefinitionContext definition = doc.definitions.lastWhere( 17 | (DefinitionContext context) => context is OperationDefinitionContext, 18 | orElse: () => null, 19 | ); 20 | 21 | if (definition != null) { 22 | if (definition.name != null) { 23 | return definition.name; 24 | } 25 | } 26 | } 27 | 28 | return null; 29 | } 30 | -------------------------------------------------------------------------------- /example/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/src/link/http/http_config.dart: -------------------------------------------------------------------------------- 1 | class HttpQueryOptions { 2 | HttpQueryOptions({ 3 | this.includeQuery, 4 | this.includeExtensions, 5 | }); 6 | 7 | bool includeQuery; 8 | bool includeExtensions; 9 | 10 | void addAll(HttpQueryOptions options) { 11 | if (options.includeQuery != null) { 12 | includeQuery = options.includeQuery; 13 | } 14 | 15 | if (options.includeExtensions != null) { 16 | includeExtensions = options.includeExtensions; 17 | } 18 | } 19 | } 20 | 21 | class HttpConfig { 22 | HttpConfig({ 23 | this.http, 24 | this.options, 25 | this.credentials, 26 | this.headers, 27 | }); 28 | 29 | HttpQueryOptions http; 30 | Map options; 31 | Map credentials; 32 | Map headers; 33 | } 34 | 35 | class HttpOptionsAndBody { 36 | HttpOptionsAndBody({ 37 | this.options, 38 | this.body, 39 | }); 40 | 41 | final Map options; 42 | final String body; 43 | } -------------------------------------------------------------------------------- /lib/src/utilities/traverse.dart: -------------------------------------------------------------------------------- 1 | typedef Object Transform(Object node); 2 | 3 | Map traverseValues( 4 | Map node, 5 | Transform transform, 6 | ) { 7 | return node.map( 8 | (String key, Object value) => MapEntry( 9 | key, 10 | traverse(value, transform), 11 | ), 12 | ); 13 | } 14 | 15 | // Attempts to apply the transform to every leaf of the data structure recursively. 16 | // Stops recursing when a node is transformed (returns non-null) 17 | Object traverse(Object node, Transform transform) { 18 | final Object transformed = transform(node); 19 | if (transformed != null) { 20 | return transformed; 21 | } 22 | 23 | if (node is List) { 24 | return node 25 | .map((Object node) => traverse(node, transform)) 26 | .toList(); 27 | } 28 | if (node is Map) { 29 | return traverseValues(node, transform); 30 | } 31 | return node; 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/link/link.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_graphql/src/link/fetch_result.dart'; 4 | import 'package:flutter_graphql/src/link/operation.dart'; 5 | 6 | typedef NextLink = Stream Function( 7 | Operation operation, 8 | ); 9 | 10 | typedef RequestHandler = Stream Function( 11 | Operation operation, [ 12 | NextLink forward, 13 | ]); 14 | 15 | Link _concat( 16 | Link first, 17 | Link second, 18 | ) { 19 | return Link(request: ( 20 | Operation operation, [ 21 | NextLink forward, 22 | ]) { 23 | return first.request(operation, (Operation op) { 24 | return second.request(op, forward); 25 | }); 26 | }); 27 | } 28 | 29 | class Link { 30 | Link({ 31 | this.request, 32 | }); 33 | 34 | final RequestHandler request; 35 | 36 | Link concat(Link next) { 37 | return _concat(this, next); 38 | } 39 | } 40 | 41 | Stream execute({ 42 | Link link, 43 | Operation operation, 44 | }) { 45 | return link.request(operation); 46 | } -------------------------------------------------------------------------------- /lib/src/link/operation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class Operation { 4 | Operation({ 5 | this.document, 6 | this.variables, 7 | this.operationName, 8 | this.extensions, 9 | }); 10 | 11 | final String document; 12 | final Map variables; 13 | final String operationName; 14 | final Map extensions; 15 | 16 | final Map _context = {}; 17 | 18 | /// Sets the context of an opration by merging the new context with the old one. 19 | void setContext(Map next) { 20 | _context.addAll(next); 21 | } 22 | 23 | Map getContext() { 24 | final Map result = {}; 25 | result.addAll(_context); 26 | 27 | return result; 28 | } 29 | 30 | String toKey() { 31 | /// XXX we're assuming here that variables will be serialized in the same order. 32 | /// that might not always be true 33 | final String encodedVariables = json.encode(variables); 34 | 35 | return '$document|$encodedVariables|$operationName'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, Zino App B.V. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/flutter_graphql.dart: -------------------------------------------------------------------------------- 1 | library flutter_graphql; 2 | 3 | export 'package:flutter_graphql/src/graphql_client.dart'; 4 | export 'package:flutter_graphql/src/socket_client.dart'; 5 | 6 | export 'package:flutter_graphql/src/core/query_options.dart'; 7 | export 'package:flutter_graphql/src/core/query_result.dart'; 8 | export 'package:flutter_graphql/src/core/graphql_error.dart'; 9 | 10 | export 'package:flutter_graphql/src/link/link.dart'; 11 | export 'package:flutter_graphql/src/link/http/link_http.dart'; 12 | 13 | export 'package:flutter_graphql/src/cache/in_memory.dart'; 14 | export 'package:flutter_graphql/src/cache/normalized_in_memory.dart'; 15 | 16 | export 'package:flutter_graphql/src/websocket/messages.dart'; 17 | export 'package:flutter_graphql/src/websocket/socket.dart'; 18 | 19 | export 'package:flutter_graphql/src/widgets/graphql_provider.dart'; 20 | export 'package:flutter_graphql/src/widgets/graphql_consumer.dart'; 21 | export 'package:flutter_graphql/src/widgets/cache_provider.dart'; 22 | export 'package:flutter_graphql/src/widgets/query.dart'; 23 | export 'package:flutter_graphql/src/widgets/mutation.dart'; 24 | export 'package:flutter_graphql/src/widgets/subscription.dart'; 25 | -------------------------------------------------------------------------------- /graphql.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/src/link/auth/auth_link.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_graphql/src/link/link.dart'; 4 | import 'package:flutter_graphql/src/link/operation.dart'; 5 | import 'package:flutter_graphql/src/link/fetch_result.dart'; 6 | 7 | typedef GetToken = Future Function(); 8 | 9 | class AuthLink extends Link { 10 | AuthLink({ 11 | this.getToken, 12 | }) : super( 13 | request: (Operation operation, [NextLink forward]) { 14 | StreamController controller; 15 | 16 | Future onListen() async { 17 | try { 18 | final String token = await getToken(); 19 | 20 | operation.setContext(>{ 21 | 'headers': {'Authorization': token} 22 | }); 23 | } catch (error) { 24 | controller.addError(error); 25 | } 26 | 27 | await controller.addStream(forward(operation)); 28 | await controller.close(); 29 | } 30 | 31 | controller = StreamController(onListen: onListen); 32 | 33 | return controller.stream; 34 | }, 35 | ); 36 | 37 | GetToken getToken; 38 | } -------------------------------------------------------------------------------- /lib/src/cache/normalized/sql/sql_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart'; 2 | import 'package:sqflite/sqflite.dart'; 3 | 4 | class SqlHelper { 5 | 6 | static const String TABLE_RECORDS = 'records'; 7 | static const String COLUMN_ID = '_id'; 8 | static const String COLUMN_RECORD = 'record'; 9 | static const String COLUMN_KEY = 'key'; 10 | 11 | static const String DATABASE_NAME = 'graphql-flutter.db'; 12 | static const int DATABASE_VERSION = 1; 13 | 14 | static const String DATABASE_CREATE = '''CREATE TABLE $TABLE_RECORDS ($COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COLUMN_KEY TEXT NOT NULL, $COLUMN_RECORD TEXT NOT NULL'''; 15 | static const String IDX_RECORDS_KEY = 'idx_records_key'; 16 | static const String CREATE_KEY_INDEX = '''CREATE INDEX $IDX_RECORDS_KEY ON $TABLE_RECORDS ($COLUMN_KEY)'''; 17 | 18 | Database db; 19 | 20 | Future open() async { 21 | final databasesPath = await getDatabasesPath(); 22 | String path = join(databasesPath, DATABASE_NAME); 23 | db = await openDatabase(path, version: DATABASE_VERSION, onCreate: (Database db, int version) async { 24 | await db.execute(DATABASE_CREATE); 25 | await db.execute(CREATE_KEY_INDEX); 26 | }); 27 | } 28 | 29 | Future close() async { 30 | await db.close(); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for request-info - https://github.com/behaviorbot/request-info 2 | 3 | # *OPTIONAL* Comment to reply with 4 | # Can be either a string : 5 | requestInfoReplyComment: > 6 | We would appreciate it if you could provide us with more info about this issue/pr! 7 | 8 | # Or an array: 9 | # requestInfoReplyComment: 10 | # - Ah no! young blade! That was a trifle short! 11 | # - Tell me more ! 12 | # - I am sure you can be more effusive 13 | 14 | # *OPTIONAL* default titles to check against for lack of descriptiveness 15 | # MUST BE ALL LOWERCASE 16 | requestInfoDefaultTitles: 17 | - update readme.md 18 | - updates 19 | 20 | # *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given 21 | requestInfoLabelToAdd: "needs more info" 22 | 23 | # *OPTIONAL* Require Pull Requests to contain more information than what is provided in the PR template 24 | # Will fail if the pull request's body is equal to the provided template 25 | checkPullRequestTemplate: true 26 | 27 | # *OPTIONAL* Only warn about insufficient information on these events type 28 | # Keys must be lowercase. Valid values are 'issue' and 'pullRequest' 29 | requestInfoOn: 30 | pullRequest: true 31 | issue: true 32 | 33 | # *OPTIONAL* Add a list of people whose Issues/PRs will not be commented on 34 | # keys must be GitHub usernames 35 | # requestInfoUserstoExclude: 36 | # - HofmannZ 37 | -------------------------------------------------------------------------------- /example/app_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 27 19 | 20 | lintOptions { 21 | disable 'InvalidPackage' 22 | } 23 | 24 | defaultConfig { 25 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 26 | applicationId "com.example.app" 27 | minSdkVersion 16 28 | targetSdkVersion 27 29 | versionCode 1 30 | versionName "1.0" 31 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 32 | } 33 | 34 | buildTypes { 35 | release { 36 | // TODO: Add your own signing config for the release build. 37 | // Signing with the debug keys for now, so `flutter run --release` works. 38 | signingConfig signingConfigs.debug 39 | } 40 | } 41 | } 42 | 43 | flutter { 44 | source '../..' 45 | } 46 | 47 | dependencies { 48 | testImplementation 'junit:junit:4.12' 49 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 50 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/widgets/graphql_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'package:flutter_graphql/src/graphql_client.dart'; 4 | 5 | class GraphQLProvider extends StatefulWidget { 6 | const GraphQLProvider({ 7 | Key key, 8 | this.client, 9 | this.child, 10 | }) : super(key: key); 11 | 12 | final ValueNotifier client; 13 | final Widget child; 14 | 15 | static ValueNotifier of(BuildContext context) { 16 | final _InheritedGraphQLProvider inheritedGraphqlProvider = 17 | context.inheritFromWidgetOfExactType(_InheritedGraphQLProvider); 18 | 19 | return inheritedGraphqlProvider.client; 20 | } 21 | 22 | @override 23 | State createState() => _GraphQLProviderState(); 24 | } 25 | 26 | class _GraphQLProviderState extends State { 27 | void didValueChange() => setState(() {}); 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | 33 | widget.client.addListener(didValueChange); 34 | } 35 | 36 | @override 37 | void dispose() { 38 | widget.client?.removeListener(didValueChange); 39 | 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return _InheritedGraphQLProvider( 46 | client: widget.client, 47 | child: widget.child, 48 | ); 49 | } 50 | } 51 | 52 | class _InheritedGraphQLProvider extends InheritedWidget { 53 | _InheritedGraphQLProvider({ 54 | this.client, 55 | Widget child, 56 | }) : clientValue = client.value, 57 | super(child: child); 58 | 59 | final ValueNotifier client; 60 | final GraphQLClient clientValue; 61 | 62 | @override 63 | bool updateShouldNotify(_InheritedGraphQLProvider oldWidget) { 64 | return clientValue != oldWidget.clientValue; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/widgets/cache_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_graphql/src/graphql_client.dart'; 4 | import 'package:flutter_graphql/src/widgets/graphql_provider.dart'; 5 | 6 | class CacheProvider extends StatefulWidget { 7 | const CacheProvider({ 8 | final Key key, 9 | @required this.child, 10 | }) : super(key: key); 11 | 12 | final Widget child; 13 | 14 | @override 15 | _CacheProviderState createState() => _CacheProviderState(); 16 | } 17 | 18 | class _CacheProviderState extends State 19 | with WidgetsBindingObserver { 20 | GraphQLClient client; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | 26 | WidgetsBinding.instance.addObserver(this); 27 | } 28 | 29 | @override 30 | void didChangeDependencies() { 31 | /// Gets the client from the closest wrapping [GraphqlProvider]. 32 | client = GraphQLProvider.of(context).value; 33 | assert(client != null); 34 | 35 | client.cache?.restore(); 36 | 37 | super.didChangeDependencies(); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | super.dispose(); 43 | 44 | WidgetsBinding.instance.removeObserver(this); 45 | } 46 | 47 | @override 48 | void didChangeAppLifecycleState(AppLifecycleState state) { 49 | assert(client != null); 50 | 51 | switch (state) { 52 | case AppLifecycleState.inactive: 53 | client.cache?.save(); 54 | break; 55 | 56 | case AppLifecycleState.paused: 57 | client.cache?.save(); 58 | break; 59 | 60 | case AppLifecycleState.suspending: 61 | break; 62 | 63 | case AppLifecycleState.resumed: 64 | client.cache?.restore(); 65 | break; 66 | } 67 | } 68 | 69 | @override 70 | Widget build(BuildContext context) => widget.child; 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/core/graphql_error.dart: -------------------------------------------------------------------------------- 1 | /// A location where a [GraphQLError] appears. 2 | class Location { 3 | /// Constructs a [Location] from a JSON map. 4 | Location.fromJSON(Map data) 5 | : line = data['line'], 6 | column = data['column']; 7 | 8 | /// The line of the error in the query. 9 | final int line; 10 | 11 | /// The column of the error in the query. 12 | final int column; 13 | 14 | @override 15 | String toString() => '{ line: $line, column: $column }'; 16 | } 17 | 18 | /// A GraphQL error (returned by a GraphQL server). 19 | class GraphQLError { 20 | GraphQLError({ 21 | this.data, 22 | this.message, 23 | this.locations, 24 | this.path, 25 | this.extensions, 26 | }); 27 | 28 | /// Constructs a [GraphQLError] from a JSON map. 29 | GraphQLError.fromJSON(this.data) 30 | : message = data['message'], 31 | locations = data['locations'] is List> 32 | ? List.from( 33 | (data['locations']).map( 34 | (Map location) => Location.fromJSON(location), 35 | ), 36 | ) 37 | : null, 38 | path = data['path'], 39 | extensions = data['extensions']; 40 | 41 | /// The message of the error. 42 | final dynamic data; 43 | 44 | /// The message of the error. 45 | final String message; 46 | 47 | /// Locations where the error appear. 48 | final List locations; 49 | 50 | /// The path of the field in error. 51 | final List path; 52 | 53 | /// Custom error data returned by your GraphQL API server 54 | final Map extensions; 55 | 56 | @override 57 | String toString() => 58 | '$message: ${locations is List ? locations.map((Location l) => '[${l.toString()}]').join('') : "Undefined location"}'; 59 | } -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/src/cache/normalized/sql/sql-normalized-cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flutter_graphql/src/cache/normalized/record_field_json_adapter.dart'; 4 | import 'package:flutter_graphql/src/cache/normalized/sql/sql_helper.dart'; 5 | import 'package:sqflite/sqflite.dart'; 6 | 7 | import '../../cache.dart'; 8 | 9 | class SqlNormalizedCache implements Cache { 10 | 11 | SqlNormalizedCache(this.dbHelper, this.recordFieldAdapter) { 12 | dbHelper.open(); 13 | database = dbHelper.db; 14 | } 15 | 16 | static const String UPDATE_STATEMENT = '''UPDATE ${SqlHelper.TABLE_RECORDS} SET ${SqlHelper.COLUMN_KEY}=?, ${SqlHelper.COLUMN_RECORD}=? WHERE ${SqlHelper.COLUMN_KEY}=?'''; 17 | static const String DELETE_STATEMENT = '''DELETE FROM ${SqlHelper.TABLE_RECORDS} WHERE ${SqlHelper.COLUMN_KEY}=?'''; 18 | static const String DELETE_ALL_RECORD_STATEMENT = '''DELETE FROM ${SqlHelper.TABLE_RECORDS}'''; 19 | 20 | Database database; 21 | final SqlHelper dbHelper; 22 | final allColumns = [ 23 | SqlHelper.COLUMN_ID, 24 | SqlHelper.COLUMN_KEY, 25 | SqlHelper.COLUMN_RECORD]; 26 | final RecordFieldJsonAdapter recordFieldAdapter; 27 | HashMap _inMemoryCache = HashMap(); 28 | 29 | @override 30 | Object read(String key) { 31 | // TODO: implement read 32 | return null; 33 | } 34 | 35 | Future>> _readFromStorage() async { 36 | List> records = await database.query(SqlHelper.TABLE_RECORDS); 37 | return records; 38 | } 39 | 40 | @override 41 | void reset() { 42 | // TODO: implement reset 43 | } 44 | 45 | @override 46 | void restore() { 47 | // TODO: implement restore 48 | } 49 | 50 | @override 51 | void save() { 52 | // TODO: implement save 53 | } 54 | 55 | @override 56 | void write(String key, dynamic values) { 57 | database.insert(SqlHelper.TABLE_RECORDS, values); 58 | } 59 | 60 | @override 61 | Future remove(String key, bool cascade) async { 62 | assert(key != null); 63 | final deletedObj = await database.delete(SqlHelper.TABLE_RECORDS, where: '${SqlHelper.COLUMN_KEY}=?', whereArgs: [key].toList()); 64 | return true; 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /test/normalized_in_memory_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:flutter_graphql/src/cache/normalized_in_memory.dart'; 3 | 4 | List reference(String key) { 5 | return ['cache/reference', key]; 6 | } 7 | 8 | const String rawOperationKey = 'rawOperationKey'; 9 | 10 | final Map rawOperationData = { 11 | 'a': { 12 | '__typename': 'A', 13 | 'id': 1, 14 | 'list': [ 15 | 1, 16 | 2, 17 | 3, 18 | { 19 | '__typename': 'Item', 20 | 'id': 4, 21 | 'value': 4, 22 | } 23 | ], 24 | 'b': { 25 | '__typename': 'B', 26 | 'id': 5, 27 | 'c': { 28 | '__typename': 'C', 29 | 'id': 6, 30 | 'cField': 'value', 31 | }, 32 | 'bField': {'field': true} 33 | }, 34 | }, 35 | 'aField': {'field': false} 36 | }; 37 | 38 | final Map updatedCValue = { 39 | '__typename': 'C', 40 | 'id': 6, 41 | 'new': 'field', 42 | 'cField': 'changed value', 43 | }; 44 | 45 | final Map updatedCOperationData = { 46 | 'a': { 47 | '__typename': 'A', 48 | 'id': 1, 49 | 'list': [ 50 | 1, 51 | 2, 52 | 3, 53 | { 54 | '__typename': 'Item', 55 | 'id': 4, 56 | 'value': 4, 57 | } 58 | ], 59 | 'b': { 60 | '__typename': 'B', 61 | 'id': 5, 62 | 'c': updatedCValue, 63 | 'bField': {'field': true} 64 | }, 65 | }, 66 | 'aField': {'field': false} 67 | }; 68 | 69 | void main() { 70 | group('Normalizes writes', () { 71 | final NormalizedInMemoryCache cache = NormalizedInMemoryCache( 72 | dataIdFromObject: typenameDataIdFromObject, 73 | ); 74 | test('.read .write round trip', () { 75 | cache.write(rawOperationKey, rawOperationData); 76 | expect(cache.read(rawOperationKey), equals(rawOperationData)); 77 | }); 78 | test('updating nested data changes top level operation', () { 79 | cache.write('C/6', updatedCValue); 80 | expect(cache.read(rawOperationKey), equals(updatedCOperationData)); 81 | }); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf .symlinks') 33 | system('mkdir -p .symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/src/graphql_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'package:flutter_graphql/src/core/query_manager.dart'; 6 | import 'package:flutter_graphql/src/core/query_result.dart'; 7 | import 'package:flutter_graphql/src/core/observable_query.dart'; 8 | import 'package:flutter_graphql/src/core/query_options.dart'; 9 | 10 | import 'package:flutter_graphql/src/link/link.dart'; 11 | import 'package:flutter_graphql/src/cache/cache.dart'; 12 | 13 | /// The link is a [Link] over which GraphQL documents will be resolved into a [FetchResult]. 14 | /// The cache is the initial [Cache] to use in the data store. 15 | class GraphQLClient { 16 | /// The [Link] over which GraphQL documents will be resolved into a [FetchResult]. 17 | final Link link; 18 | 19 | /// The initial [Cache] to use in the data store. 20 | final Cache cache; 21 | 22 | QueryManager queryManager; 23 | 24 | /// Constructs a [GraphQLClient] given a [Link] and a [Cache]. 25 | GraphQLClient({ 26 | @required this.link, 27 | @required this.cache, 28 | }) { 29 | queryManager = QueryManager( 30 | link: link, 31 | cache: cache, 32 | ); 33 | } 34 | 35 | /// This registers a query in the [QueryManager] and returns an [ObservableQuery] 36 | /// based on the provided [WatchQueryOptions]. 37 | ObservableQuery watchQuery(WatchQueryOptions options) { 38 | return queryManager.watchQuery(options); 39 | } 40 | 41 | /// This resolves a single query according to the [QueryOptions] specified and 42 | /// returns a [Future] which resolves with the [QueryResult] or throws an [Exception]. 43 | Future query(QueryOptions options) { 44 | return queryManager.query(options); 45 | } 46 | 47 | /// This resolves a single mutation according to the [MutationOptions] specified and 48 | /// returns a [Future] which resolves with the [QueryResult] or throws an [Exception]. 49 | Future mutate(MutationOptions options) { 50 | return queryManager.mutate(options); 51 | } 52 | 53 | /// This subscribes to a GraphQL subscription according to the options specified and returns an 54 | /// [Stream] which either emits received data or an error. 55 | Stream subscribe(dynamic options) { 56 | // TODO: merge the subscription client with the new client 57 | return const Stream.empty(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/socket_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:uuid/uuid.dart'; 5 | 6 | import 'package:flutter_graphql/flutter_graphql.dart'; 7 | 8 | SocketClient socketClient; 9 | 10 | class SocketClient { 11 | final Uuid _uuid = Uuid(); 12 | final GraphQLSocket _socket; 13 | static Map _initPayload; 14 | 15 | SocketClient(this._socket) { 16 | _socket.connectionAck.listen(print); 17 | _socket.connectionError.listen(print); 18 | _socket.unknownData.listen(print); 19 | _socket.write(InitOperation(_initPayload)); 20 | } 21 | 22 | static Future connect( 23 | final String endPoint, { 24 | final List protocols = const [ 25 | 'graphql-ws', 26 | ], 27 | final Map headers = const { 28 | 'content-type': 'application/json', 29 | }, 30 | final Map initPayload, 31 | }) async { 32 | _initPayload = initPayload; 33 | 34 | return SocketClient( 35 | GraphQLSocket( 36 | await WebSocket.connect( 37 | endPoint, 38 | protocols: protocols, 39 | headers: headers, 40 | ), 41 | ), 42 | ); 43 | } 44 | 45 | Stream subscribe(final SubscriptionRequest payload) { 46 | final String id = _uuid.v4(); 47 | 48 | final StreamController response = 49 | StreamController(); 50 | 51 | final Stream complete = _socket.subscriptionComplete 52 | .where((SubscriptionComplete message) => message.id == id) 53 | .take(1); 54 | 55 | final Stream data = _socket.subscriptionData 56 | .where((SubscriptionData message) => message.id == id) 57 | .takeWhile((_) => !response.isClosed); 58 | 59 | final Stream error = _socket.subscriptionError 60 | .where((SubscriptionError message) => message.id == id) 61 | .takeWhile((_) => !response.isClosed); 62 | 63 | complete.listen((_) => response.close()); 64 | data.listen((SubscriptionData message) => response.add(message)); 65 | error.listen((SubscriptionError message) => response.addError(message)); 66 | 67 | response.onListen = () => _socket.write(StartOperation(id, payload)); 68 | response.onCancel = () => _socket.write(StopOperation(id)); 69 | 70 | return response.stream; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/src/cache/normalized_in_memory.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:flutter_graphql/src/utilities/traverse.dart'; 3 | import 'package:flutter_graphql/src/cache/in_memory.dart'; 4 | typedef String DataIdFromObject(Object node); 5 | 6 | class NormalizationException implements Exception { 7 | NormalizationException(this.cause, this.overflowError, this.value); 8 | 9 | StackOverflowError overflowError; 10 | String cause; 11 | Object value; 12 | 13 | String get message => cause; 14 | } 15 | 16 | class NormalizedInMemoryCache extends InMemoryCache { 17 | NormalizedInMemoryCache({ 18 | @required this.dataIdFromObject, 19 | String prefix = '@cache/reference', 20 | }) : _prefix = prefix; 21 | 22 | DataIdFromObject dataIdFromObject; 23 | String _prefix; 24 | 25 | Object _dereference(Object node) { 26 | if (node is List && node.length == 2 && node[0] == _prefix) { 27 | return read(node[1]); 28 | } 29 | 30 | return null; 31 | } 32 | 33 | /* 34 | Derefrences object references, 35 | replacing them with cached instances 36 | */ 37 | @override 38 | dynamic read(String key) { 39 | final Object value = super.read(key); 40 | 41 | try { 42 | return traverse(value, _dereference); 43 | } catch (error) { 44 | if (error is StackOverflowError) { 45 | throw NormalizationException( 46 | ''' 47 | Dereferencing failed for $value this is likely caused by a circular reference. 48 | Please ensure dataIdFromObject returns a unique identifier for all possible entities in your system 49 | ''', 50 | error, 51 | value, 52 | ); 53 | } 54 | } 55 | } 56 | 57 | List _normalize(Object node) { 58 | final String dataId = dataIdFromObject(node); 59 | 60 | if (dataId != null) { 61 | write(dataId, node); 62 | return [_prefix, dataId]; 63 | } 64 | 65 | return null; 66 | } 67 | 68 | /* 69 | Writes included objects to store, 70 | replacing them with references 71 | */ 72 | @override 73 | void write(String key, Object value) { 74 | final Object normalized = traverseValues(value, _normalize); 75 | super.write(key, normalized); 76 | } 77 | } 78 | 79 | String typenameDataIdFromObject(Object object) { 80 | if (object is Map && 81 | object.containsKey('__typename') && 82 | object.containsKey('id')) { 83 | return "${object['__typename']}/${object['id']}"; 84 | } 85 | 86 | return null; 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/widgets/subscription.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | import '../socket_client.dart'; 6 | import '../websocket/messages.dart'; 7 | 8 | typedef OnSubscriptionCompleted = void Function(); 9 | 10 | typedef SubscriptionBuilder = Widget Function({ 11 | final bool loading, 12 | final dynamic payload, 13 | final dynamic error, 14 | }); 15 | 16 | class Subscription extends StatefulWidget { 17 | const Subscription( 18 | this.operationName, 19 | this.query, { 20 | this.variables = const {}, 21 | final Key key, 22 | @required this.builder, 23 | this.initial, 24 | this.onCompleted, 25 | }) : super(key: key); 26 | 27 | final String operationName; 28 | final String query; 29 | final dynamic variables; 30 | final SubscriptionBuilder builder; 31 | final OnSubscriptionCompleted onCompleted; 32 | final dynamic initial; 33 | 34 | @override 35 | _SubscriptionState createState() => _SubscriptionState(); 36 | } 37 | 38 | class _SubscriptionState extends State { 39 | bool _loading = true; 40 | dynamic _data; 41 | dynamic _error; 42 | 43 | bool _alive = true; 44 | 45 | @override 46 | void initState() { 47 | super.initState(); 48 | 49 | final Stream stream = socketClient.subscribe( 50 | SubscriptionRequest( 51 | widget.operationName, widget.query, widget.variables)); 52 | 53 | stream.takeWhile((SubscriptionData message) => _alive).listen( 54 | _onData, 55 | onError: _onError, 56 | onDone: _onDone, 57 | ); 58 | 59 | if (widget.initial != null) { 60 | setState(() { 61 | _loading = true; 62 | _data = widget.initial; 63 | _error = null; 64 | }); 65 | } 66 | } 67 | 68 | @override 69 | void dispose() { 70 | _alive = false; 71 | super.dispose(); 72 | } 73 | 74 | void _onData(final SubscriptionData message) { 75 | setState(() { 76 | _loading = false; 77 | _data = message.data; 78 | _error = message.errors; 79 | }); 80 | } 81 | 82 | void _onError(final Object error) { 83 | setState(() { 84 | _loading = false; 85 | _data = null; 86 | _error = (error is SubscriptionError) ? error.payload : error; 87 | }); 88 | } 89 | 90 | void _onDone() { 91 | if (widget.onCompleted != null) { 92 | widget.onCompleted(); 93 | } 94 | } 95 | 96 | @override 97 | Widget build(final BuildContext context) { 98 | return widget.builder( 99 | loading: _loading, 100 | error: _error, 101 | payload: _data, 102 | ); 103 | } 104 | } -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /lib/src/widgets/query.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'package:flutter_graphql/src/graphql_client.dart'; 4 | import 'package:flutter_graphql/src/core/observable_query.dart'; 5 | import 'package:flutter_graphql/src/core/query_options.dart'; 6 | import 'package:flutter_graphql/src/core/query_result.dart'; 7 | 8 | import 'package:flutter_graphql/src/widgets/graphql_provider.dart'; 9 | 10 | typedef QueryBuilder = Widget Function(QueryResult result); 11 | 12 | /// Builds a [Query] widget based on the a given set of [QueryOptions] 13 | /// that streams [QueryResult]s into the [QueryBuilder]. 14 | class Query extends StatefulWidget { 15 | const Query({ 16 | final Key key, 17 | @required this.options, 18 | @required this.builder, 19 | }) : super(key: key); 20 | 21 | final QueryOptions options; 22 | final QueryBuilder builder; 23 | 24 | @override 25 | QueryState createState() => QueryState(); 26 | } 27 | 28 | class QueryState extends State { 29 | ObservableQuery observableQuery; 30 | 31 | WatchQueryOptions get _options { 32 | FetchPolicy fetchPolicy = widget.options.fetchPolicy; 33 | 34 | if (fetchPolicy == FetchPolicy.cacheFirst) { 35 | fetchPolicy = FetchPolicy.cacheAndNetwork; 36 | } 37 | 38 | return WatchQueryOptions( 39 | document: widget.options.document, 40 | variables: widget.options.variables, 41 | fetchPolicy: fetchPolicy, 42 | errorPolicy: widget.options.errorPolicy, 43 | pollInterval: widget.options.pollInterval, 44 | fetchResults: true, 45 | context: widget.options.context, 46 | client: widget.options.client 47 | ); 48 | } 49 | 50 | void _initQuery() { 51 | GraphQLClient client; 52 | 53 | if (_options.client != null) 54 | client =_options.client; 55 | else 56 | client = GraphQLProvider.of(context).value; 57 | assert(client != null); 58 | 59 | observableQuery?.close(); 60 | observableQuery = client.watchQuery(_options); 61 | } 62 | 63 | @override 64 | void didChangeDependencies() { 65 | super.didChangeDependencies(); 66 | _initQuery(); 67 | } 68 | 69 | @override 70 | void didUpdateWidget(Query oldWidget) { 71 | super.didUpdateWidget(oldWidget); 72 | 73 | // TODO @micimize - investigate why/if this was causing issues 74 | if (!observableQuery.options.areEqualTo(_options)) { 75 | _initQuery(); 76 | } 77 | } 78 | 79 | @override 80 | void dispose() { 81 | observableQuery?.close(); 82 | super.dispose(); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return StreamBuilder( 88 | initialData: QueryResult( 89 | loading: true, 90 | ), 91 | stream: observableQuery.stream, 92 | builder: ( 93 | BuildContext buildContext, 94 | AsyncSnapshot snapshot, 95 | ) { 96 | return widget?.builder(snapshot.data); 97 | }, 98 | ); 99 | } 100 | } -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at development@zinoapp.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /lib/src/scheduler/scheduler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_graphql/src/core/query_manager.dart'; 4 | import 'package:flutter_graphql/src/core/query_options.dart'; 5 | import 'package:flutter_graphql/src/core/observable_query.dart'; 6 | 7 | class QueryScheduler { 8 | QueryScheduler({ 9 | this.queryManager, 10 | }); 11 | 12 | QueryManager queryManager; 13 | 14 | /// Map going from query ids to the [WatchQueryOptions] associated with those queries. 15 | Map registeredQueries = 16 | {}; 17 | 18 | /// Map going from poling interval to the query ids that fire on that interval. 19 | /// These query ids are associated with a [ObservableQuery] in the registeredQueries. 20 | Map> intervalQueries = >{}; 21 | 22 | /// Map going from polling interval durations to polling timers. 23 | final Map _pollingTimers = {}; 24 | 25 | void fetchQueriesOnInterval( 26 | Timer timer, 27 | Duration interval, 28 | ) { 29 | intervalQueries[interval].retainWhere( 30 | (String queryId) { 31 | // If ObservableQuery can't be found from registeredQueries or if it has a 32 | // different interval, it means that this queryId is no longer registered 33 | // and should be removed from the list of queries firing on this interval. 34 | // 35 | // We don't remove queries from intervalQueries immediately in 36 | // stopPollingQuery so that we can keep the timer consistent when queries 37 | // are removed and replaced, and to avoid quadratic behavior when stopping 38 | // many queries. 39 | if (registeredQueries[queryId] == null) { 40 | return false; 41 | } 42 | 43 | final Duration pollInterval = 44 | Duration(seconds: registeredQueries[queryId].pollInterval); 45 | 46 | return registeredQueries.containsKey(queryId) && 47 | pollInterval == interval; 48 | }, 49 | ); 50 | 51 | // if no queries on the interval clean up 52 | if (intervalQueries[interval].isEmpty) { 53 | intervalQueries.remove(interval); 54 | _pollingTimers.remove(interval); 55 | timer.cancel(); 56 | return; 57 | } 58 | 59 | // fetch each query on the interval 60 | for (String queryId in intervalQueries[interval]) { 61 | final WatchQueryOptions options = registeredQueries[queryId]; 62 | queryManager.fetchQuery(queryId, options); 63 | } 64 | } 65 | 66 | void startPollingQuery( 67 | WatchQueryOptions options, 68 | String queryId, 69 | ) { 70 | registeredQueries[queryId] = options; 71 | 72 | final Duration interval = Duration( 73 | seconds: options.pollInterval, 74 | ); 75 | 76 | if (intervalQueries.containsKey(interval)) { 77 | intervalQueries[interval].add(queryId); 78 | } else { 79 | intervalQueries[interval] = [queryId]; 80 | 81 | _pollingTimers[interval] = Timer.periodic( 82 | interval, 83 | (Timer timer) => fetchQueriesOnInterval(timer, interval), 84 | ); 85 | } 86 | } 87 | 88 | /// Removes the [ObservableQuery] from one of the registered queries. 89 | /// The fetchQueriesOnInterval will then take care of not firing it anymore. 90 | void stopPollingQuery(String queryId) { 91 | registeredQueries.remove(queryId); 92 | } 93 | } -------------------------------------------------------------------------------- /lib/src/websocket/socket.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import '../../flutter_graphql.dart'; 6 | 7 | /// Wraps a standard web socket instance to marshal and un-marshal the server / 8 | /// client payloads into dart object representation. 9 | class GraphQLSocket { 10 | GraphQLSocket(this._socket) { 11 | _socket 12 | .map>((dynamic message) => json.decode(message)) 13 | .listen( 14 | (Map message) { 15 | final String type = message['type'] ?? 'unknown'; 16 | final dynamic payload = message['payload'] ?? {}; 17 | final String id = message['id'] ?? 'none'; 18 | 19 | switch (type) { 20 | case MessageTypes.GQL_CONNECTION_ACK: 21 | _subject.add(ConnectionAck()); 22 | break; 23 | case MessageTypes.GQL_CONNECTION_ERROR: 24 | _subject.add(ConnectionError(payload)); 25 | break; 26 | case MessageTypes.GQL_CONNECTION_KEEP_ALIVE: 27 | _subject.add(ConnectionKeepAlive()); 28 | break; 29 | case MessageTypes.GQL_DATA: 30 | final dynamic data = payload['data']; 31 | final dynamic errors = payload['errors']; 32 | _subject.add(SubscriptionData(id, data, errors)); 33 | break; 34 | case MessageTypes.GQL_ERROR: 35 | _subject.add(SubscriptionError(id, payload)); 36 | break; 37 | case MessageTypes.GQL_COMPLETE: 38 | _subject.add(SubscriptionComplete(id)); 39 | break; 40 | default: 41 | _subject.add(UnknownData(message)); 42 | } 43 | }, 44 | ); 45 | } 46 | 47 | final StreamController _subject = 48 | StreamController.broadcast(); 49 | 50 | final WebSocket _socket; 51 | 52 | void write(final GraphQLSocketMessage message) { 53 | _socket.add( 54 | json.encode( 55 | message, 56 | toEncodable: (dynamic m) => m.toJson(), 57 | ), 58 | ); 59 | } 60 | 61 | Stream get connectionAck => _subject.stream 62 | .where((GraphQLSocketMessage message) => message is ConnectionAck) 63 | .cast(); 64 | 65 | Stream get connectionKeepAlive => _subject.stream 66 | .where((GraphQLSocketMessage message) => message is ConnectionKeepAlive) 67 | .cast(); 68 | 69 | Stream get connectionError => _subject.stream 70 | .where((GraphQLSocketMessage message) => message is ConnectionError) 71 | .cast(); 72 | 73 | Stream get unknownData => _subject.stream 74 | .where((GraphQLSocketMessage message) => message is UnknownData) 75 | .cast(); 76 | 77 | Stream get subscriptionData => _subject.stream 78 | .where((GraphQLSocketMessage message) => message is SubscriptionData) 79 | .cast(); 80 | 81 | Stream get subscriptionError => _subject.stream 82 | .where((GraphQLSocketMessage message) => message is SubscriptionError) 83 | .cast(); 84 | 85 | Stream get subscriptionComplete => _subject.stream 86 | .where((GraphQLSocketMessage message) => message is SubscriptionComplete) 87 | .cast(); 88 | } 89 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /lib/src/widgets/mutation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'package:flutter_graphql/src/graphql_client.dart'; 4 | import 'package:flutter_graphql/src/core/observable_query.dart'; 5 | import 'package:flutter_graphql/src/core/query_options.dart'; 6 | import 'package:flutter_graphql/src/core/query_result.dart'; 7 | import 'package:flutter_graphql/src/cache/cache.dart'; 8 | import 'package:flutter_graphql/src/utilities/helpers.dart'; 9 | 10 | import 'package:flutter_graphql/src/widgets/graphql_provider.dart'; 11 | 12 | typedef RunMutation = void Function(Map variables); 13 | typedef MutationBuilder = Widget Function( 14 | RunMutation runMutation, 15 | QueryResult result, 16 | ); 17 | 18 | typedef void OnMutationCompleted(QueryResult result); 19 | typedef void OnMutationUpdate(Cache cache, QueryResult result); 20 | 21 | /// Builds a [Mutation] widget based on the a given set of [MutationOptions] 22 | /// that streams [QueryResult]s into the [QueryBuilder]. 23 | class Mutation extends StatefulWidget { 24 | const Mutation({ 25 | final Key key, 26 | @required this.options, 27 | @required this.builder, 28 | this.onCompleted, 29 | this.update, 30 | }) : super(key: key); 31 | 32 | final MutationOptions options; 33 | final MutationBuilder builder; 34 | final OnMutationCompleted onCompleted; 35 | final OnMutationUpdate update; 36 | 37 | @override 38 | MutationState createState() => MutationState(); 39 | } 40 | 41 | class MutationState extends State { 42 | GraphQLClient client; 43 | ObservableQuery observableQuery; 44 | 45 | WatchQueryOptions get _options => WatchQueryOptions( 46 | document: widget.options.document, 47 | variables: widget.options.variables, 48 | fetchPolicy: widget.options.fetchPolicy, 49 | errorPolicy: widget.options.errorPolicy, 50 | fetchResults: false, 51 | context: widget.options.context, 52 | client: widget.options.client 53 | ); 54 | 55 | // TODO is it possible to extract shared logic into mixin 56 | void _initQuery() { 57 | if (_options.client != null) 58 | client =_options.client; 59 | else 60 | client = GraphQLProvider.of(context).value; 61 | assert(client != null); 62 | 63 | observableQuery?.close(); 64 | observableQuery = client.watchQuery(_options); 65 | } 66 | 67 | @override 68 | void didChangeDependencies() { 69 | super.didChangeDependencies(); 70 | _initQuery(); 71 | } 72 | 73 | @override 74 | void didUpdateWidget(Mutation oldWidget) { 75 | super.didUpdateWidget(oldWidget); 76 | 77 | // TODO @micimize - investigate why/if this was causing issues 78 | if (!observableQuery.options.areEqualTo(_options)) { 79 | _initQuery(); 80 | } 81 | } 82 | 83 | OnData get update { 84 | // fallback client in case widget has been disposed of 85 | final Cache cache = client.cache; 86 | if (widget.update != null) { 87 | void updateOnData(QueryResult result) { 88 | widget.update(client?.cache ?? cache, result); 89 | } 90 | 91 | return updateOnData; 92 | } 93 | return null; 94 | } 95 | 96 | Iterable get callbacks { 97 | return [widget.onCompleted, update].where(notNull); 98 | } 99 | 100 | void runMutation(Map variables) => observableQuery 101 | ..setVariables(variables) 102 | ..onData(callbacks) // add callbacks to observable 103 | ..sendLoading() 104 | ..fetchResults(); 105 | 106 | @override 107 | void dispose() { 108 | observableQuery?.close(force: false); 109 | super.dispose(); 110 | } 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | return StreamBuilder( 115 | initialData: QueryResult( 116 | loading: false, 117 | ), 118 | stream: observableQuery?.stream, 119 | builder: ( 120 | BuildContext buildContext, 121 | AsyncSnapshot snapshot, 122 | ) { 123 | return widget.builder( 124 | runMutation, 125 | snapshot.data, 126 | ); 127 | }, 128 | ); 129 | } 130 | } -------------------------------------------------------------------------------- /lib/src/cache/in_memory.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | import 'dart:convert'; 4 | import 'dart:io'; 5 | 6 | import 'package:flutter_graphql/src/cache/cache.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | 9 | class InMemoryCache implements Cache { 10 | InMemoryCache({ 11 | this.customStorageDirectory, 12 | }); 13 | 14 | /// A directory to be used for storage. 15 | /// This is used for testing, on regular usage 16 | /// 'path_provider' will provide the storage directory. 17 | final Directory customStorageDirectory; 18 | 19 | HashMap _inMemoryCache = HashMap(); 20 | bool _writingToStorage = false; 21 | 22 | /// Reads an entity from the internal HashMap. 23 | @override 24 | dynamic read(String key) { 25 | if (_inMemoryCache.containsKey(key)) { 26 | return _inMemoryCache[key]; 27 | } 28 | 29 | return null; 30 | } 31 | 32 | /// Writes an entity to the internal HashMap. 33 | @override 34 | void write(String key, dynamic value) { 35 | if (_inMemoryCache.containsKey(key) && 36 | _inMemoryCache[key] is Map && 37 | value != null && 38 | value is Map) { 39 | // Avoid overriding a superset with a subset of a field (#155) 40 | _inMemoryCache[key].addAll(value); 41 | } else { 42 | _inMemoryCache[key] = value; 43 | } 44 | } 45 | 46 | /// Saves the internal HashMap to a file. 47 | @override 48 | Future save() async { 49 | await _writeToStorage(); 50 | } 51 | 52 | /// Restores the internal HashMap to a file. 53 | @override 54 | Future restore() async { 55 | _inMemoryCache = await _readFromStorage(); 56 | } 57 | 58 | /// Clears the internal HashMap. 59 | @override 60 | void reset() { 61 | _inMemoryCache.clear(); 62 | } 63 | 64 | Future get _localStoragePath async { 65 | if (customStorageDirectory != null) { 66 | // Used for testing 67 | return customStorageDirectory.path; 68 | } 69 | 70 | final Directory directory = await getApplicationDocumentsDirectory(); 71 | 72 | return directory.path; 73 | } 74 | 75 | Future get _localStorageFile async { 76 | final String path = await _localStoragePath; 77 | 78 | return File('$path/cache.txt'); 79 | } 80 | 81 | Future _writeToStorage() async { 82 | if (_writingToStorage) { 83 | return; 84 | } 85 | 86 | _writingToStorage = true; 87 | 88 | // Catching errors to avoid locking forever. 89 | // Maybe the device couldn't write in the past 90 | // but it may in the future. 91 | try { 92 | final File file = await _localStorageFile; 93 | final IOSink sink = file.openWrite(); 94 | _inMemoryCache.forEach((String key, dynamic value) { 95 | sink.writeln(json.encode([key, value])); 96 | }); 97 | 98 | await sink.close(); 99 | 100 | _writingToStorage = false; 101 | } catch (err) { 102 | _writingToStorage = false; 103 | 104 | rethrow; 105 | } 106 | return; 107 | } 108 | 109 | Future> _readFromStorage() async { 110 | try { 111 | final File file = await _localStorageFile; 112 | final HashMap storedHashMap = HashMap(); 113 | 114 | if (file.existsSync()) { 115 | final Stream> inputStream = file.openRead(); 116 | 117 | await for (String line in inputStream 118 | .transform(utf8.decoder) // Decode bytes to UTF8. 119 | .transform( 120 | const LineSplitter(), 121 | )) { 122 | final List keyAndValue = json.decode(line); 123 | storedHashMap[keyAndValue[0]] = keyAndValue[1]; 124 | } 125 | } 126 | 127 | return storedHashMap; 128 | } on FileSystemException { 129 | // TODO: handle no such file 130 | print('Can\'t read file from storage, returning an empty HashMap.'); 131 | 132 | return HashMap(); 133 | } catch (error) { 134 | // TODO: handle error 135 | print(error); 136 | 137 | return HashMap(); 138 | } 139 | } 140 | 141 | @override 142 | Future remove(String key, bool cascade) { 143 | // TODO: implement remove 144 | return null; 145 | } 146 | } -------------------------------------------------------------------------------- /lib/src/core/observable_query.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_graphql/flutter_graphql.dart'; 4 | import 'package:flutter_graphql/src/core/query_manager.dart'; 5 | import 'package:flutter_graphql/src/core/query_result.dart'; 6 | import 'package:flutter_graphql/src/scheduler/scheduler.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | 10 | typedef void OnData(QueryResult result); 11 | 12 | enum QueryLifecycle { 13 | UNEXECUTED, 14 | PENDING, 15 | POLLING, 16 | POLLING_STOPPED, 17 | SIDE_EFFECTS_PENDING, 18 | SIDE_EFFECTS_BLOCKING, 19 | 20 | // right now only Mutations ever become completed 21 | COMPLETED, 22 | } 23 | 24 | class ObservableQuery { 25 | ObservableQuery({ 26 | @required this.queryManager, 27 | @required this.options, 28 | }) : queryId = queryManager.generateQueryId().toString(), 29 | scheduler = queryManager.scheduler { 30 | controller = StreamController.broadcast( 31 | onListen: onListen, 32 | ); 33 | } 34 | 35 | final String queryId; 36 | final QueryScheduler scheduler; 37 | final QueryManager queryManager; 38 | 39 | final Set> _onDataSubscriptions = 40 | Set>(); 41 | 42 | QueryLifecycle lifecycle = QueryLifecycle.UNEXECUTED; 43 | 44 | WatchQueryOptions options; 45 | 46 | StreamController controller; 47 | 48 | Stream get stream => controller.stream; 49 | bool get isCurrentlyPolling => lifecycle == QueryLifecycle.POLLING; 50 | 51 | void onListen() { 52 | if (options.fetchResults) { 53 | fetchResults(); 54 | } 55 | } 56 | 57 | void fetchResults() { 58 | queryManager.fetchQuery(queryId, options); 59 | 60 | // if onData callbacks have been registered, 61 | // they should be waited on by default 62 | lifecycle = _onDataSubscriptions.isNotEmpty 63 | ? QueryLifecycle.SIDE_EFFECTS_PENDING 64 | : QueryLifecycle.PENDING; 65 | 66 | if (options.pollInterval != null) { 67 | startPolling(options.pollInterval); 68 | } 69 | } 70 | 71 | void sendLoading() { 72 | controller.add( 73 | QueryResult( 74 | loading: true, 75 | ), 76 | ); 77 | } 78 | 79 | // most mutation behavior happens here 80 | void onData(Iterable callbacks) { 81 | if (callbacks != null && callbacks.isNotEmpty) { 82 | StreamSubscription subscription; 83 | 84 | subscription = stream.listen((QueryResult result) { 85 | void handle(OnData callback) { 86 | callback(result); 87 | } 88 | 89 | if (!result.loading) { 90 | callbacks.forEach(handle); 91 | subscription.cancel(); 92 | _onDataSubscriptions.remove(subscription); 93 | 94 | if (_onDataSubscriptions.isEmpty) { 95 | if (lifecycle == QueryLifecycle.SIDE_EFFECTS_BLOCKING) { 96 | lifecycle = QueryLifecycle.COMPLETED; 97 | close(); 98 | } 99 | 100 | lifecycle = QueryLifecycle.COMPLETED; 101 | } 102 | } 103 | }); 104 | 105 | _onDataSubscriptions.add(subscription); 106 | } 107 | } 108 | 109 | void startPolling(int pollInterval) { 110 | if (options.fetchPolicy == FetchPolicy.cacheFirst || 111 | options.fetchPolicy == FetchPolicy.cacheOnly) { 112 | throw Exception( 113 | 'Queries that specify the cacheFirst and cacheOnly fetch policies cannot also be polling queries.', 114 | ); 115 | } 116 | 117 | if (isCurrentlyPolling) { 118 | scheduler.stopPollingQuery(queryId); 119 | } 120 | 121 | options.pollInterval = pollInterval; 122 | lifecycle = QueryLifecycle.POLLING; 123 | scheduler.startPollingQuery(options, queryId); 124 | } 125 | 126 | void stopPolling() { 127 | if (isCurrentlyPolling) { 128 | scheduler.stopPollingQuery(queryId); 129 | options.pollInterval = null; 130 | lifecycle = QueryLifecycle.POLLING_STOPPED; 131 | } 132 | } 133 | 134 | void setVariables(Map variables) { 135 | options.variables = variables; 136 | } 137 | 138 | Future close({bool force = false, bool fromManager = false}) async { 139 | if (lifecycle == QueryLifecycle.SIDE_EFFECTS_PENDING && !force) { 140 | lifecycle = QueryLifecycle.SIDE_EFFECTS_BLOCKING; 141 | return null; 142 | } 143 | 144 | if (!fromManager) { 145 | queryManager.closeQuery(this, fromQuery: true); 146 | } 147 | 148 | for (StreamSubscription subscription in _onDataSubscriptions) { 149 | subscription.cancel(); 150 | } 151 | 152 | stopPolling(); 153 | await controller.close(); 154 | } 155 | } -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "flutter-graphql", 3 | "projectOwner": "juicycleff", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "contributors": [ 12 | { 13 | "login": "juicycleff", 14 | "name": "Rex Raphael", 15 | "avatar_url": "https://avatars2.githubusercontent.com/u/4757453?v=4", 16 | "profile": "http://rexraphael.com", 17 | "contributions": [ 18 | "bug", 19 | "code", 20 | "doc", 21 | "example", 22 | "ideas", 23 | "review" 24 | ] 25 | }, 26 | { 27 | "login": "eusdima", 28 | "name": "Eustatiu Dima", 29 | "avatar_url": "https://avatars2.githubusercontent.com/u/4757453?v=4", 30 | "profile": "http://eusdima.com", 31 | "contributions": [ 32 | "bug", 33 | "code", 34 | "doc", 35 | "example", 36 | "ideas", 37 | "review" 38 | ] 39 | }, 40 | { 41 | "login": "HofmannZ", 42 | "name": "Zino Hofmann", 43 | "avatar_url": "https://avatars3.githubusercontent.com/u/17142193?v=4", 44 | "profile": "https://github.com/HofmannZ", 45 | "contributions": [ 46 | "bug", 47 | "code", 48 | "doc", 49 | "example", 50 | "ideas", 51 | "infra", 52 | "review" 53 | ] 54 | }, 55 | { 56 | "login": "jinxac", 57 | "name": "Harkirat Saluja", 58 | "avatar_url": "https://avatars2.githubusercontent.com/u/15068096?v=4", 59 | "profile": "https://github.com/jinxac", 60 | "contributions": [ 61 | "doc", 62 | "ideas" 63 | ] 64 | }, 65 | { 66 | "login": "camuthig", 67 | "name": "Chris Muthig", 68 | "avatar_url": "https://avatars3.githubusercontent.com/u/5178217?v=4", 69 | "profile": "https://github.com/camuthig", 70 | "contributions": [ 71 | "code", 72 | "doc", 73 | "example", 74 | "ideas" 75 | ] 76 | }, 77 | { 78 | "login": "cal-pratt", 79 | "name": "Cal Pratt", 80 | "avatar_url": "https://avatars1.githubusercontent.com/u/7611406?v=4", 81 | "profile": "http://stackoverflow.com/users/3280538/flkes", 82 | "contributions": [ 83 | "bug", 84 | "code", 85 | "doc", 86 | "example", 87 | "ideas" 88 | ] 89 | }, 90 | { 91 | "login": "mmadjer", 92 | "name": "Miroslav Valkovic-Madjer", 93 | "avatar_url": "https://avatars0.githubusercontent.com/u/9830761?v=4", 94 | "profile": "http://madjer.info", 95 | "contributions": [ 96 | "code" 97 | ] 98 | }, 99 | { 100 | "login": "AleksandarFaraj", 101 | "name": "Aleksandar Faraj", 102 | "avatar_url": "https://avatars2.githubusercontent.com/u/4523129?v=4", 103 | "profile": "https://github.com/AleksandarFaraj", 104 | "contributions": [ 105 | "bug" 106 | ] 107 | }, 108 | { 109 | "login": "adelcasse", 110 | "name": "Arnaud Delcasse", 111 | "avatar_url": "https://avatars0.githubusercontent.com/u/403029?v=4", 112 | "profile": "https://www.scity.coop", 113 | "contributions": [ 114 | "bug", 115 | "code" 116 | ] 117 | }, 118 | { 119 | "login": "dustin-graham", 120 | "name": "Dustin Graham", 121 | "avatar_url": "https://avatars0.githubusercontent.com/u/959931?v=4", 122 | "profile": "https://github.com/dustin-graham", 123 | "contributions": [ 124 | "bug", 125 | "code" 126 | ] 127 | }, 128 | { 129 | "login": "fabiocarneiro", 130 | "name": "Fábio Carneiro", 131 | "avatar_url": "https://avatars3.githubusercontent.com/u/1375034?v=4", 132 | "profile": "https://github.com/fabiocarneiro", 133 | "contributions": [ 134 | "bug" 135 | ] 136 | }, 137 | { 138 | "login": "lordgreg", 139 | "name": "Gregor", 140 | "avatar_url": "https://avatars0.githubusercontent.com/u/480546?v=4", 141 | "profile": "https://github.com/lordgreg", 142 | "contributions": [ 143 | "bug", 144 | "code", 145 | "ideas" 146 | ] 147 | }, 148 | { 149 | "login": "kolja-esders", 150 | "name": "Kolja Esders", 151 | "avatar_url": "https://avatars1.githubusercontent.com/u/5159563?v=4", 152 | "profile": "https://github.com/kolja-esders", 153 | "contributions": [ 154 | "bug", 155 | "code", 156 | "ideas" 157 | ] 158 | }, 159 | { 160 | "login": "micimize", 161 | "name": "Michael Joseph Rosenthal", 162 | "avatar_url": "https://avatars1.githubusercontent.com/u/8343799?v=4", 163 | "profile": "https://github.com/micimize", 164 | "contributions": [ 165 | "bug", 166 | "code", 167 | "doc", 168 | "example", 169 | "ideas", 170 | "test" 171 | ] 172 | }, 173 | { 174 | "login": "Igor1201", 175 | "name": "Igor Borges", 176 | "avatar_url": "https://avatars2.githubusercontent.com/u/735858?v=4", 177 | "profile": "http://borges.me/", 178 | "contributions": [ 179 | "bug", 180 | "code" 181 | ] 182 | }, 183 | { 184 | "login": "rafaelring", 185 | "name": "Rafael Ring", 186 | "avatar_url": "https://avatars1.githubusercontent.com/u/6992724?v=4", 187 | "profile": "https://github.com/rafaelring", 188 | "contributions": [ 189 | "bug", 190 | "code" 191 | ] 192 | } 193 | ] 194 | } 195 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /lib/src/core/query_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_graphql/src/graphql_client.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | /// [FetchPolicy] determines where the client may return a result from. The options are: 5 | /// - cacheFirst (default): return result from cache. Only fetch from network if cached result is not available. 6 | /// - cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available. 7 | /// - cacheOnly: return result from cache if available, fail otherwise. 8 | /// - noCache: return result from network, fail if network call doesn't succeed, don't save to cache. 9 | /// - networkOnly: return result from network, fail if network call doesn't succeed, save to cache. 10 | enum FetchPolicy { 11 | cacheFirst, 12 | cacheAndNetwork, 13 | cacheOnly, 14 | noCache, 15 | networkOnly, 16 | } 17 | 18 | /// [ErrorPolicy] determines the level of events for errors in the execution result. The options are: 19 | /// - none (default): any errors from the request are treated like runtime errors and the observable is stopped. 20 | /// - ignore: errors from the request do not stop the observable, but also don't call `next`. 21 | /// - all: errors are treated like data and will notify observables. 22 | enum ErrorPolicy { 23 | none, 24 | ignore, 25 | all, 26 | } 27 | 28 | /// Base options. 29 | class BaseOptions { 30 | BaseOptions({ 31 | @required this.document, 32 | this.variables, 33 | this.fetchPolicy, 34 | this.errorPolicy, 35 | this.context, 36 | this.client, 37 | }); 38 | 39 | /// A GraphQL document that consists of a single query to be sent down to the server. 40 | String document; 41 | 42 | /// A map going from variable name to variable value, where the variables are used 43 | /// within the GraphQL query. 44 | Map variables; 45 | 46 | /// Specifies the [FetchPolicy] to be used. 47 | FetchPolicy fetchPolicy; 48 | 49 | /// Specifies the [ErrorPolicy] to be used. 50 | ErrorPolicy errorPolicy; 51 | 52 | /// Context to be passed to link execution chain. 53 | Map context; 54 | 55 | GraphQLClient client; 56 | } 57 | 58 | /// Query options. 59 | class QueryOptions extends BaseOptions { 60 | QueryOptions({ 61 | @required String document, 62 | Map variables, 63 | FetchPolicy fetchPolicy = FetchPolicy.cacheFirst, 64 | ErrorPolicy errorPolicy = ErrorPolicy.none, 65 | this.pollInterval, 66 | Map context, 67 | GraphQLClient client, 68 | }) : super( 69 | document: document, 70 | variables: variables, 71 | fetchPolicy: fetchPolicy, 72 | errorPolicy: errorPolicy, 73 | context: context, 74 | client: client 75 | ); 76 | 77 | /// The time interval (in milliseconds) on which this query should be 78 | /// refetched from the server. 79 | int pollInterval; 80 | } 81 | 82 | /// Mutation options 83 | class MutationOptions extends BaseOptions { 84 | MutationOptions({ 85 | @required String document, 86 | Map variables, 87 | FetchPolicy fetchPolicy = FetchPolicy.networkOnly, 88 | ErrorPolicy errorPolicy = ErrorPolicy.none, 89 | Map context, 90 | GraphQLClient client 91 | }) : super( 92 | document: document, 93 | variables: variables, 94 | fetchPolicy: fetchPolicy, 95 | errorPolicy: errorPolicy, 96 | context: context, 97 | client: client 98 | ); 99 | } 100 | 101 | // ObservableQuery options 102 | class WatchQueryOptions extends QueryOptions { 103 | WatchQueryOptions({ 104 | @required String document, 105 | Map variables, 106 | FetchPolicy fetchPolicy = FetchPolicy.cacheAndNetwork, 107 | ErrorPolicy errorPolicy = ErrorPolicy.none, 108 | int pollInterval, 109 | this.fetchResults, 110 | Map context, 111 | GraphQLClient client, 112 | }) : super( 113 | document: document, 114 | variables: variables, 115 | fetchPolicy: fetchPolicy, 116 | errorPolicy: errorPolicy, 117 | pollInterval: pollInterval, 118 | context: context, 119 | client: client 120 | ); 121 | 122 | /// Whether or not to fetch result. 123 | bool fetchResults; 124 | 125 | /// Checks if the [WatchQueryOptions] in this class are equal to some given options. 126 | bool areEqualTo(WatchQueryOptions otherOptions) { 127 | return !_areDifferentOptions(this, otherOptions); 128 | } 129 | 130 | /// Checks if two options are equal. 131 | bool _areDifferentOptions( 132 | WatchQueryOptions a, 133 | WatchQueryOptions b, 134 | ) { 135 | if (a.document != b.document) { 136 | return true; 137 | } 138 | 139 | if (a.fetchPolicy != b.fetchPolicy) { 140 | return true; 141 | } 142 | 143 | if (a.errorPolicy != b.errorPolicy) { 144 | return true; 145 | } 146 | 147 | if (a.pollInterval != b.pollInterval) { 148 | return true; 149 | } 150 | 151 | if (a.fetchResults != b.fetchResults) { 152 | return true; 153 | } 154 | 155 | // compare variables last, because maps take more time 156 | return _areDifferentVariables(a.variables, b.variables); 157 | } 158 | 159 | bool _areDifferentVariables( 160 | Map a, 161 | Map b, 162 | ) { 163 | if (a == null && b == null) { 164 | return false; 165 | } 166 | 167 | if (a == null || b == null) { 168 | return true; 169 | } 170 | 171 | if (a.length != b.length) { 172 | return true; 173 | } 174 | 175 | bool areDifferent = false; 176 | 177 | a.forEach((String key, dynamic value) { 178 | if ((!b.containsKey(key)) || b[key] != value) { 179 | areDifferent = true; 180 | } 181 | }); 182 | 183 | return areDifferent; 184 | } 185 | } -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-dynamic: false 4 | errors: 5 | # treat missing required parameters as a warning (not a hint) 6 | missing_required_param: warning 7 | # treat missing returns as a warning (not a hint) 8 | missing_return: warning 9 | 10 | linter: 11 | rules: 12 | # these rules are documented on and in the same order as 13 | # the Dart Lint rules page to make maintenance easier 14 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 15 | - always_declare_return_types 16 | - always_put_control_body_on_new_line 17 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 18 | - always_require_non_null_named_parameters 19 | - always_specify_types 20 | - annotate_overrides 21 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 22 | - avoid_as 23 | # - avoid_bool_literals_in_conditional_expressions # not yet tested 24 | # - avoid_catches_without_on_clauses # we do this commonly 25 | # - avoid_catching_errors # we do this commonly 26 | - avoid_classes_with_only_static_members 27 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 28 | - avoid_empty_else 29 | - avoid_field_initializers_in_const_classes 30 | - avoid_function_literals_in_foreach_calls 31 | - avoid_init_to_null 32 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 33 | - avoid_null_checks_in_equality_operators 34 | # - avoid_positional_boolean_parameters # not yet tested 35 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 36 | - avoid_relative_lib_imports 37 | - avoid_renaming_method_parameters 38 | - avoid_return_types_on_setters 39 | # - avoid_returning_null # there are plenty of valid reasons to return null 40 | # - avoid_returning_this # there are plenty of valid reasons to return this 41 | # - avoid_setters_without_getters # not yet tested 42 | # - avoid_single_cascade_in_expression_statements # not yet tested 43 | - avoid_slow_async_io 44 | - avoid_types_as_parameter_names 45 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 46 | - avoid_unused_constructor_parameters 47 | - await_only_futures 48 | - camel_case_types 49 | - cancel_subscriptions 50 | # - cascade_invocations # not yet tested 51 | # - close_sinks # not reliable enough 52 | # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 53 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 54 | - control_flow_in_finally 55 | - directives_ordering 56 | - empty_catches 57 | - empty_constructor_bodies 58 | - empty_statements 59 | - hash_and_equals 60 | - implementation_imports 61 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 62 | - iterable_contains_unrelated_type 63 | # - join_return_with_assignment # not yet tested 64 | - library_names 65 | - library_prefixes 66 | - list_remove_unrelated_type 67 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 68 | - no_adjacent_strings_in_list 69 | - no_duplicate_case_values 70 | - non_constant_identifier_names 71 | # - omit_local_variable_types # opposite of always_specify_types 72 | # - one_member_abstracts # too many false positives 73 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 74 | - overridden_fields 75 | - package_api_docs 76 | - package_names 77 | - package_prefixed_library_names 78 | # - parameter_assignments # we do this commonly 79 | - prefer_adjacent_string_concatenation 80 | - prefer_asserts_in_initializer_lists 81 | - prefer_bool_in_asserts 82 | - prefer_collection_literals 83 | - prefer_conditional_assignment 84 | - prefer_const_constructors 85 | - prefer_const_constructors_in_immutables 86 | - prefer_const_declarations 87 | - prefer_const_literals_to_create_immutables 88 | # - prefer_constructors_over_static_methods # not yet tested 89 | - prefer_contains 90 | - prefer_equal_for_default_values 91 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 92 | - prefer_final_fields 93 | - prefer_final_locals 94 | - prefer_foreach 95 | # - prefer_function_declarations_over_variables # not yet tested 96 | - prefer_initializing_formals 97 | # - prefer_interpolation_to_compose_strings # not yet tested 98 | - prefer_iterable_whereType 99 | - prefer_is_empty 100 | - prefer_is_not_empty 101 | - prefer_single_quotes 102 | - prefer_typing_uninitialized_variables 103 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 104 | - recursive_getters 105 | - slash_for_doc_comments 106 | - sort_constructors_first 107 | - sort_unnamed_constructors_first 108 | - super_goes_last 109 | - test_types_in_equals 110 | - throw_in_finally 111 | # - type_annotate_public_apis # subset of always_specify_types 112 | - type_init_formals 113 | # - unawaited_futures # too many false positives 114 | - unnecessary_brace_in_string_interps 115 | - unnecessary_const 116 | - unnecessary_getters_setters 117 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 118 | - unnecessary_null_aware_assignments 119 | - unnecessary_null_in_if_null_operators 120 | - unnecessary_overrides 121 | - unnecessary_parenthesis 122 | - unnecessary_statements 123 | - unnecessary_this 124 | - unrelated_type_equality_checks 125 | - use_rethrow_when_possible 126 | # - use_setters_to_change_properties # not yet tested 127 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 128 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 129 | - valid_regexps 130 | # - void_checks # not yet tested 131 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-dynamic: false 4 | errors: 5 | # treat missing required parameters as a warning (not a hint) 6 | missing_required_param: warning 7 | # treat missing returns as a warning (not a hint) 8 | missing_return: warning 9 | 10 | linter: 11 | rules: 12 | # these rules are documented on and in the same order as 13 | # the Dart Lint rules page to make maintenance easier 14 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 15 | - always_declare_return_types 16 | - always_put_control_body_on_new_line 17 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 18 | - always_require_non_null_named_parameters 19 | - always_specify_types 20 | - annotate_overrides 21 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 22 | - avoid_as 23 | # - avoid_bool_literals_in_conditional_expressions # not yet tested 24 | # - avoid_catches_without_on_clauses # we do this commonly 25 | # - avoid_catching_errors # we do this commonly 26 | - avoid_classes_with_only_static_members 27 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 28 | - avoid_empty_else 29 | - avoid_field_initializers_in_const_classes 30 | - avoid_function_literals_in_foreach_calls 31 | - avoid_init_to_null 32 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 33 | - avoid_null_checks_in_equality_operators 34 | # - avoid_positional_boolean_parameters # not yet tested 35 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 36 | - avoid_relative_lib_imports 37 | - avoid_renaming_method_parameters 38 | - avoid_return_types_on_setters 39 | # - avoid_returning_null # there are plenty of valid reasons to return null 40 | # - avoid_returning_this # there are plenty of valid reasons to return this 41 | # - avoid_setters_without_getters # not yet tested 42 | # - avoid_single_cascade_in_expression_statements # not yet tested 43 | - avoid_slow_async_io 44 | - avoid_types_as_parameter_names 45 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 46 | - avoid_unused_constructor_parameters 47 | - await_only_futures 48 | - camel_case_types 49 | - cancel_subscriptions 50 | # - cascade_invocations # not yet tested 51 | # - close_sinks # not reliable enough 52 | # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 53 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 54 | - control_flow_in_finally 55 | - directives_ordering 56 | - empty_catches 57 | - empty_constructor_bodies 58 | - empty_statements 59 | - hash_and_equals 60 | - implementation_imports 61 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 62 | - iterable_contains_unrelated_type 63 | # - join_return_with_assignment # not yet tested 64 | - library_names 65 | - library_prefixes 66 | - list_remove_unrelated_type 67 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 68 | - no_adjacent_strings_in_list 69 | - no_duplicate_case_values 70 | - non_constant_identifier_names 71 | # - omit_local_variable_types # opposite of always_specify_types 72 | # - one_member_abstracts # too many false positives 73 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 74 | - overridden_fields 75 | - package_api_docs 76 | - package_names 77 | - package_prefixed_library_names 78 | # - parameter_assignments # we do this commonly 79 | - prefer_adjacent_string_concatenation 80 | - prefer_asserts_in_initializer_lists 81 | - prefer_bool_in_asserts 82 | - prefer_collection_literals 83 | - prefer_conditional_assignment 84 | - prefer_const_constructors 85 | - prefer_const_constructors_in_immutables 86 | - prefer_const_declarations 87 | - prefer_const_literals_to_create_immutables 88 | # - prefer_constructors_over_static_methods # not yet tested 89 | - prefer_contains 90 | - prefer_equal_for_default_values 91 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 92 | - prefer_final_fields 93 | - prefer_final_locals 94 | - prefer_foreach 95 | # - prefer_function_declarations_over_variables # not yet tested 96 | - prefer_initializing_formals 97 | # - prefer_interpolation_to_compose_strings # not yet tested 98 | - prefer_iterable_whereType 99 | - prefer_is_empty 100 | - prefer_is_not_empty 101 | - prefer_single_quotes 102 | - prefer_typing_uninitialized_variables 103 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 104 | - recursive_getters 105 | - slash_for_doc_comments 106 | - sort_constructors_first 107 | - sort_unnamed_constructors_first 108 | - super_goes_last 109 | - test_types_in_equals 110 | - throw_in_finally 111 | # - type_annotate_public_apis # subset of always_specify_types 112 | - type_init_formals 113 | # - unawaited_futures # too many false positives 114 | - unnecessary_brace_in_string_interps 115 | - unnecessary_const 116 | - unnecessary_getters_setters 117 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 118 | - unnecessary_null_aware_assignments 119 | - unnecessary_null_in_if_null_operators 120 | - unnecessary_overrides 121 | - unnecessary_parenthesis 122 | - unnecessary_statements 123 | - unnecessary_this 124 | - unrelated_type_equality_checks 125 | - use_rethrow_when_possible 126 | # - use_setters_to_change_properties # not yet tested 127 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 128 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 129 | - valid_regexps 130 | # - void_checks # not yet tested 131 | -------------------------------------------------------------------------------- /lib/src/core/query_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'package:flutter_graphql/src/core/query_options.dart'; 6 | import 'package:flutter_graphql/src/core/query_result.dart'; 7 | import 'package:flutter_graphql/src/core/graphql_error.dart'; 8 | import 'package:flutter_graphql/src/core/observable_query.dart'; 9 | 10 | import 'package:flutter_graphql/src/scheduler/scheduler.dart'; 11 | 12 | import 'package:flutter_graphql/src/link/link.dart'; 13 | import 'package:flutter_graphql/src/link/operation.dart'; 14 | import 'package:flutter_graphql/src/link/fetch_result.dart'; 15 | 16 | import 'package:flutter_graphql/src/cache/cache.dart'; 17 | 18 | import 'package:flutter_graphql/src/utilities/get_from_ast.dart'; 19 | 20 | class QueryManager { 21 | QueryManager({ 22 | @required this.link, 23 | @required this.cache, 24 | }) { 25 | scheduler = QueryScheduler( 26 | queryManager: this, 27 | ); 28 | } 29 | 30 | final Link link; 31 | final Cache cache; 32 | 33 | QueryScheduler scheduler; 34 | int idCounter = 1; 35 | Map queries = {}; 36 | 37 | ObservableQuery watchQuery(WatchQueryOptions options) { 38 | if (options.document == null) { 39 | throw Exception( 40 | 'document option is required. You must specify your GraphQL document in the query options.', 41 | ); 42 | } 43 | 44 | final ObservableQuery observableQuery = ObservableQuery( 45 | queryManager: this, 46 | options: options, 47 | ); 48 | 49 | setQuery(observableQuery); 50 | 51 | return observableQuery; 52 | } 53 | 54 | Future query(QueryOptions options) { 55 | return fetchQuery('0', options); 56 | } 57 | 58 | Future mutate(MutationOptions options) { 59 | return fetchQuery('0', options); 60 | } 61 | 62 | Future fetchQuery( 63 | String queryId, 64 | BaseOptions options, 65 | ) async { 66 | final ObservableQuery observableQuery = getQuery(queryId); 67 | // XXX there is a bug in the `graphql_parser` package, where this result might be 68 | // null event though the operation name is present in the document 69 | final String operationName = getOperationName(options.document); 70 | // create a new operation to fetch 71 | final Operation operation = Operation( 72 | document: options.document, 73 | variables: options.variables, 74 | operationName: operationName, 75 | ); 76 | 77 | FetchResult fetchResult; 78 | QueryResult queryResult; 79 | 80 | try { 81 | if (options.context != null) { 82 | operation.setContext(options.context); 83 | } 84 | 85 | if (options.fetchPolicy == FetchPolicy.cacheFirst || 86 | options.fetchPolicy == FetchPolicy.cacheAndNetwork || 87 | options.fetchPolicy == FetchPolicy.cacheOnly) { 88 | final dynamic cachedData = cache.read(operation.toKey()); 89 | 90 | if (cachedData != null) { 91 | fetchResult = FetchResult( 92 | data: cachedData, 93 | ); 94 | 95 | queryResult = _mapFetchResultToQueryResult(fetchResult); 96 | 97 | // add the result to an observable query if it exists 98 | if (observableQuery != null) { 99 | observableQuery.controller.add(queryResult); 100 | } 101 | 102 | if (options.fetchPolicy == FetchPolicy.cacheFirst || 103 | options.fetchPolicy == FetchPolicy.cacheOnly) { 104 | return queryResult; 105 | } 106 | } 107 | 108 | if (options.fetchPolicy == FetchPolicy.cacheOnly) { 109 | throw Exception( 110 | 'Could not find that operation in the cache. (${options.fetchPolicy.toString()})', 111 | ); 112 | } 113 | } 114 | 115 | // execute the operation through the provided link(s) 116 | fetchResult = await execute( 117 | link: link, 118 | operation: operation, 119 | ).first; 120 | 121 | // save the data from fetchResult to the cache 122 | if (fetchResult.data != null && 123 | options.fetchPolicy != FetchPolicy.noCache) { 124 | cache.write( 125 | operation.toKey(), 126 | fetchResult.data, 127 | ); 128 | } 129 | 130 | if (fetchResult.data == null && 131 | fetchResult.errors == null && 132 | (options.fetchPolicy == FetchPolicy.noCache || 133 | options.fetchPolicy == FetchPolicy.networkOnly)) { 134 | throw Exception( 135 | 'Could not resolve that operation on the network. (${options.fetchPolicy.toString()})', 136 | ); 137 | } 138 | 139 | queryResult = _mapFetchResultToQueryResult(fetchResult); 140 | } catch (error) { 141 | // TODO some dart errors break this 142 | final GraphQLError graphQLError = GraphQLError( 143 | message: error.message, 144 | ); 145 | 146 | if (queryResult != null) { 147 | queryResult.addError(graphQLError); 148 | } else { 149 | queryResult = QueryResult( 150 | loading: false, 151 | ); 152 | queryResult.addError(graphQLError); 153 | } 154 | } 155 | 156 | // add the result to an observable query if it exists and not closed 157 | if (observableQuery != null && !observableQuery.controller.isClosed) { 158 | observableQuery.controller.add(queryResult); 159 | } 160 | 161 | return queryResult; 162 | } 163 | 164 | ObservableQuery getQuery(String queryId) { 165 | if (queries.containsKey(queryId)) { 166 | return queries[queryId]; 167 | } 168 | 169 | return null; 170 | } 171 | 172 | void setQuery(ObservableQuery observableQuery) { 173 | queries[observableQuery.queryId] = observableQuery; 174 | } 175 | 176 | void closeQuery(ObservableQuery observableQuery, {bool fromQuery = false}) { 177 | if (!fromQuery) { 178 | observableQuery.close(fromManager: true); 179 | } 180 | queries.remove(observableQuery.queryId); 181 | } 182 | 183 | int generateQueryId() { 184 | final int requestId = idCounter; 185 | 186 | idCounter++; 187 | 188 | return requestId; 189 | } 190 | 191 | QueryResult _mapFetchResultToQueryResult(FetchResult fetchResult) { 192 | List errors; 193 | 194 | if (fetchResult.errors != null) { 195 | errors = List.from(fetchResult.errors.map( 196 | (dynamic rawError) => GraphQLError.fromJSON(rawError), 197 | )); 198 | } 199 | 200 | return QueryResult( 201 | data: fetchResult.data, 202 | errors: errors, 203 | loading: false, 204 | ); 205 | } 206 | } -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_graphql/flutter_graphql.dart'; 4 | 5 | import './mutations/addStar.dart' as mutations; 6 | import './queries/readRepositories.dart' as queries; 7 | 8 | const String YOUR_PERSONAL_ACCESS_TOKEN = ''; 9 | 10 | void main() => runApp(MyApp()); 11 | 12 | class MyApp extends StatelessWidget { 13 | @override 14 | Widget build(BuildContext context) { 15 | final HttpLink link = HttpLink( 16 | uri: 'https://api.github.com/graphql', 17 | headers: { 18 | 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', 19 | }, 20 | ); 21 | 22 | final ValueNotifier client = ValueNotifier( 23 | GraphQLClient( 24 | cache: InMemoryCache(), 25 | link: link, 26 | ), 27 | ); 28 | 29 | return GraphQLProvider( 30 | client: client, 31 | child: CacheProvider( 32 | child: MaterialApp( 33 | title: 'GraphQL Flutter Demo', 34 | theme: ThemeData( 35 | primarySwatch: Colors.blue, 36 | ), 37 | home: const MyHomePage(title: 'GraphQL Flutter Home Page'), 38 | ), 39 | ), 40 | ); 41 | } 42 | } 43 | 44 | class MyHomePage extends StatefulWidget { 45 | const MyHomePage({ 46 | Key key, 47 | this.title, 48 | }) : super(key: key); 49 | 50 | final String title; 51 | 52 | @override 53 | _MyHomePageState createState() => _MyHomePageState(); 54 | } 55 | 56 | class _MyHomePageState extends State { 57 | int nRepositories = 50; 58 | 59 | void changeQuery(String number) { 60 | setState(() { 61 | nRepositories = int.parse(number) ?? 50; 62 | }); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return Scaffold( 68 | appBar: AppBar( 69 | title: Text(widget.title), 70 | ), 71 | body: Container( 72 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 73 | child: Column( 74 | mainAxisAlignment: MainAxisAlignment.start, 75 | mainAxisSize: MainAxisSize.max, 76 | children: [ 77 | TextField( 78 | decoration: const InputDecoration( 79 | labelText: 'Number of repositories (default 50)', 80 | ), 81 | keyboardType: TextInputType.number, 82 | onSubmitted: changeQuery, 83 | ), 84 | Query( 85 | options: QueryOptions( 86 | document: queries.readRepositories, 87 | variables: { 88 | 'nRepositories': nRepositories, 89 | }, 90 | pollInterval: 4, 91 | // you can optionally override some http options through the contexts 92 | context: { 93 | 'headers': { 94 | 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', 95 | }, 96 | }, 97 | ), 98 | builder: (QueryResult result) { 99 | if (result.loading) { 100 | return const Center( 101 | child: CircularProgressIndicator(), 102 | ); 103 | } 104 | 105 | if (result.hasErrors) { 106 | return Text('\nErrors: \n ' + result.errors.join(',\n ')); 107 | } 108 | 109 | // result.data can be either a [List] or a [Map] 110 | final List repositories = 111 | result.data['viewer']['repositories']['nodes']; 112 | 113 | return Expanded( 114 | child: ListView.builder( 115 | itemCount: repositories.length, 116 | itemBuilder: (BuildContext context, int index) { 117 | final Map repository = 118 | repositories[index]; 119 | 120 | return Mutation( 121 | options: MutationOptions( 122 | document: mutations.addStar, 123 | ), 124 | builder: ( 125 | RunMutation addStar, 126 | QueryResult addStarResult, 127 | ) { 128 | if (addStarResult.data != null && 129 | addStarResult.data.isNotEmpty) { 130 | repository['viewerHasStarred'] = 131 | addStarResult.data['addStar']['starrable'] 132 | ['viewerHasStarred']; 133 | } 134 | 135 | return ListTile( 136 | leading: repository['viewerHasStarred'] 137 | ? const Icon( 138 | Icons.star, 139 | color: Colors.amber, 140 | ) 141 | : const Icon(Icons.star_border), 142 | title: Text(repository['name']), 143 | onTap: () { 144 | // optimistic ui updates are not implemented yet, therefore changes may take some time to show 145 | addStar({ 146 | 'starrableId': repository['id'], 147 | }); 148 | }, 149 | ); 150 | }, 151 | onCompleted: (QueryResult onCompleteResult) { 152 | showDialog( 153 | context: context, 154 | builder: (BuildContext context) { 155 | return AlertDialog( 156 | title: const Text('Thanks for your star!'), 157 | actions: [ 158 | SimpleDialogOption( 159 | child: const Text('Dismiss'), 160 | onPressed: () { 161 | Navigator.of(context).pop(); 162 | }, 163 | ) 164 | ], 165 | ); 166 | }, 167 | ); 168 | }, 169 | ); 170 | }, 171 | ), 172 | ); 173 | }, 174 | ), 175 | ], 176 | ), 177 | ), 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/src/websocket/messages.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | /// These messages represent the structures used for Client-server communication 4 | /// in a GraphQL web-socket subscription. Each message is represented in a JSON 5 | /// format where the data type is denoted by the `type` field. 6 | 7 | /// A list of constants used for identifying message types 8 | class MessageTypes { 9 | MessageTypes._(); 10 | 11 | // client connections 12 | static const String GQL_CONNECTION_INIT = 'connection_init'; 13 | static const String GQL_CONNECTION_TERMINATE = 'connection_terminate'; 14 | 15 | // server connections 16 | static const String GQL_CONNECTION_ACK = 'connection_ack'; 17 | static const String GQL_CONNECTION_ERROR = 'connection_error'; 18 | static const String GQL_CONNECTION_KEEP_ALIVE = 'ka'; 19 | 20 | // client operations 21 | static const String GQL_START = 'start'; 22 | static const String GQL_STOP = 'stop'; 23 | 24 | // server operations 25 | static const String GQL_DATA = 'data'; 26 | static const String GQL_ERROR = 'error'; 27 | static const String GQL_COMPLETE = 'complete'; 28 | 29 | // default tag for use in identifying issues 30 | static const String GQL_UNKNOWN = 'unknown'; 31 | } 32 | 33 | abstract class JsonSerializable { 34 | dynamic toJson(); 35 | 36 | @override 37 | String toString() => toJson().toString(); 38 | } 39 | 40 | /// Base type for representing a server-client subscription message. 41 | abstract class GraphQLSocketMessage extends JsonSerializable { 42 | GraphQLSocketMessage(this.type); 43 | 44 | final String type; 45 | } 46 | 47 | /// After establishing a connection with the server, the client will 48 | /// send this message to tell the server that it is ready to begin sending 49 | /// new subscription queries. 50 | class InitOperation extends GraphQLSocketMessage { 51 | InitOperation(this.payload) : super(MessageTypes.GQL_CONNECTION_INIT); 52 | 53 | final Map payload; 54 | 55 | @override 56 | dynamic toJson() { 57 | final Map jsonMap = {}; 58 | jsonMap['type'] = type; 59 | 60 | if (payload != null) { 61 | jsonMap['payload'] = json.encode(payload); 62 | } 63 | 64 | return json.encode(jsonMap); 65 | } 66 | } 67 | 68 | /// Represent the payload used during a Start query operation. 69 | /// The operationName should match one of the top level query definitions 70 | /// defined in the query provided. Additional variables can be provided 71 | /// and sent to the server for processing. 72 | class SubscriptionRequest extends JsonSerializable { 73 | SubscriptionRequest(this.operationName, this.query, this.variables); 74 | 75 | final String operationName; 76 | final String query; 77 | final dynamic variables; 78 | 79 | @override 80 | dynamic toJson() => { 81 | 'operationName': operationName, 82 | 'query': query, 83 | 'variables': variables, 84 | }; 85 | } 86 | 87 | /// A message to tell the server to create a subscription. The contents of the 88 | /// query will be defined by the payload request. The id provided will be used 89 | /// to tag messages such that they can be identified for this subscription 90 | /// instance. id values should be unique and not be re-used during the lifetime 91 | /// of the server. 92 | class StartOperation extends GraphQLSocketMessage { 93 | StartOperation(this.id, this.payload) : super(MessageTypes.GQL_START); 94 | 95 | final String id; 96 | final SubscriptionRequest payload; 97 | 98 | @override 99 | dynamic toJson() => { 100 | 'type': type, 101 | 'id': id, 102 | 'payload': payload, 103 | }; 104 | } 105 | 106 | /// Tell the server to stop sending subscription data for a particular 107 | /// subscription instance. See StartOperation 108 | class StopOperation extends GraphQLSocketMessage { 109 | StopOperation(this.id) : super(MessageTypes.GQL_STOP); 110 | 111 | final String id; 112 | 113 | @override 114 | dynamic toJson() => { 115 | 'type': type, 116 | 'id': id, 117 | }; 118 | } 119 | 120 | /// The server will send this acknowledgment message after receiving the init 121 | /// command from the client if the init was successful. 122 | class ConnectionAck extends GraphQLSocketMessage { 123 | ConnectionAck() : super(MessageTypes.GQL_CONNECTION_ACK); 124 | 125 | @override 126 | dynamic toJson() => { 127 | 'type': type, 128 | }; 129 | } 130 | 131 | /// The server will send this error message after receiving the init command 132 | /// from the client if the init was not successful. 133 | class ConnectionError extends GraphQLSocketMessage { 134 | ConnectionError(this.payload) : super(MessageTypes.GQL_CONNECTION_ERROR); 135 | 136 | final dynamic payload; 137 | 138 | @override 139 | dynamic toJson() => { 140 | 'type': type, 141 | 'payload': payload, 142 | }; 143 | } 144 | 145 | /// The server will send this message to keep the connection alive 146 | class ConnectionKeepAlive extends GraphQLSocketMessage { 147 | ConnectionKeepAlive() : super(MessageTypes.GQL_CONNECTION_KEEP_ALIVE); 148 | 149 | @override 150 | dynamic toJson() => { 151 | 'type': type, 152 | }; 153 | } 154 | 155 | /// Data sent from the server to the client with subscription data or error 156 | /// payload. The user should check the errors result before processing the 157 | /// data value. These error are from the query resolvers. 158 | class SubscriptionData extends GraphQLSocketMessage { 159 | SubscriptionData(this.id, this.data, this.errors) 160 | : super(MessageTypes.GQL_DATA); 161 | 162 | final String id; 163 | final dynamic data; 164 | final dynamic errors; 165 | 166 | @override 167 | dynamic toJson() => { 168 | 'type': type, 169 | 'data': data, 170 | 'errors': errors, 171 | }; 172 | } 173 | 174 | /// Errors sent from the server to the client if the subscription operation was 175 | /// not successful, usually due to GraphQL validation errors. 176 | class SubscriptionError extends GraphQLSocketMessage { 177 | SubscriptionError(this.id, this.payload) : super(MessageTypes.GQL_ERROR); 178 | 179 | final String id; 180 | final dynamic payload; 181 | 182 | @override 183 | dynamic toJson() => { 184 | 'type': type, 185 | 'id': id, 186 | 'payload': payload, 187 | }; 188 | } 189 | 190 | /// Server message to the client to indicate that no more data will be sent 191 | /// for a particular subscription instance. 192 | class SubscriptionComplete extends GraphQLSocketMessage { 193 | SubscriptionComplete(this.id) : super(MessageTypes.GQL_COMPLETE); 194 | 195 | final String id; 196 | 197 | @override 198 | dynamic toJson() => { 199 | 'type': type, 200 | 'id': id, 201 | }; 202 | } 203 | 204 | /// Not expected to be created. Indicates there are problems parsing the server 205 | /// response, or that new unsupported types have been added to the subscription 206 | /// implementation. 207 | class UnknownData extends GraphQLSocketMessage { 208 | UnknownData(this.payload) : super(MessageTypes.GQL_UNKNOWN); 209 | 210 | final dynamic payload; 211 | 212 | @override 213 | dynamic toJson() => { 214 | 'type': type, 215 | 'payload': payload, 216 | }; 217 | } -------------------------------------------------------------------------------- /lib/src/link/http/link_http.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:meta/meta.dart'; 5 | import 'package:http/http.dart'; 6 | import 'package:http_parser/http_parser.dart'; 7 | 8 | import 'package:flutter_graphql/src/link/link.dart'; 9 | import 'package:flutter_graphql/src/link/operation.dart'; 10 | import 'package:flutter_graphql/src/link/fetch_result.dart'; 11 | import 'package:flutter_graphql/src/link/http/http_config.dart'; 12 | import 'package:flutter_graphql/src/link/http/fallback_http_config.dart'; 13 | 14 | class HttpLink extends Link { 15 | HttpLink({ 16 | @required String uri, 17 | bool includeExtensions, 18 | Client fetch, 19 | Map headers, 20 | Map credentials, 21 | Map fetchOptions, 22 | }) : super( 23 | request: ( 24 | Operation operation, [ 25 | NextLink forward, 26 | ]) { 27 | final Client fetcher = fetch ?? Client(); 28 | 29 | final HttpConfig linkConfig = HttpConfig( 30 | http: HttpQueryOptions( 31 | includeExtensions: includeExtensions, 32 | ), 33 | options: fetchOptions, 34 | credentials: credentials, 35 | headers: headers, 36 | ); 37 | 38 | final Map context = operation.getContext(); 39 | HttpConfig contextConfig; 40 | 41 | if (context != null) { 42 | // TODO: refactor context to use a [HttpConfig] object to avoid dynamic types 43 | contextConfig = HttpConfig( 44 | http: HttpQueryOptions( 45 | includeExtensions: context['includeExtensions'], 46 | ), 47 | options: context['fetchOptions'], 48 | credentials: context['credentials'], 49 | headers: context['headers'], 50 | ); 51 | } 52 | 53 | final HttpOptionsAndBody httpOptionsAndBody = 54 | _selectHttpOptionsAndBody( 55 | operation, 56 | fallbackHttpConfig, 57 | linkConfig, 58 | contextConfig, 59 | ); 60 | 61 | final Map options = httpOptionsAndBody.options; 62 | final Map httpHeaders = options['headers']; 63 | 64 | StreamController controller; 65 | 66 | Future onListen() async { 67 | Response response; 68 | 69 | try { 70 | // TODO: support multiple http methods 71 | response = await fetcher.post( 72 | uri, 73 | headers: httpHeaders, 74 | body: httpOptionsAndBody.body, 75 | ); 76 | 77 | operation.setContext({ 78 | 'response': response, 79 | }); 80 | 81 | final FetchResult parsedResponse = _parseResponse(response); 82 | 83 | controller.add(parsedResponse); 84 | } catch (error) { 85 | controller.addError(error); 86 | } 87 | 88 | await controller.close(); 89 | } 90 | 91 | controller = StreamController(onListen: onListen); 92 | 93 | return controller.stream; 94 | }, 95 | ); 96 | } 97 | 98 | HttpOptionsAndBody _selectHttpOptionsAndBody( 99 | Operation operation, 100 | HttpConfig fallbackConfig, [ 101 | HttpConfig linkConfig, 102 | HttpConfig contextConfig, 103 | ]) { 104 | final Map options = { 105 | 'headers': {}, 106 | 'credentials': {}, 107 | }; 108 | final HttpQueryOptions http = HttpQueryOptions(); 109 | 110 | // http options 111 | 112 | // initialze with fallback http options 113 | http.addAll(fallbackConfig.http); 114 | 115 | // inject the configured http options 116 | if (linkConfig.http != null) { 117 | http.addAll(linkConfig.http); 118 | } 119 | 120 | // override with context http options 121 | if (contextConfig.http != null) { 122 | http.addAll(contextConfig.http); 123 | } 124 | 125 | // options 126 | 127 | // initialze with fallback options 128 | options.addAll(fallbackConfig.options); 129 | 130 | // inject the configured options 131 | if (linkConfig.options != null) { 132 | options.addAll(linkConfig.options); 133 | } 134 | 135 | // override with context options 136 | if (contextConfig.options != null) { 137 | options.addAll(contextConfig.options); 138 | } 139 | 140 | // headers 141 | 142 | // initialze with fallback headers 143 | options['headers'].addAll(fallbackConfig.headers); 144 | 145 | // inject the configured headers 146 | if (linkConfig.headers != null) { 147 | options['headers'].addAll(linkConfig.headers); 148 | } 149 | 150 | // inject the context headers 151 | if (contextConfig.headers != null) { 152 | options['headers'].addAll(contextConfig.headers); 153 | } 154 | 155 | // credentials 156 | 157 | // initialze with fallback credentials 158 | options['credentials'].addAll(fallbackConfig.credentials); 159 | 160 | // inject the configured credentials 161 | if (linkConfig.credentials != null) { 162 | options['credentials'].addAll(linkConfig.credentials); 163 | } 164 | 165 | // inject the context credentials 166 | if (contextConfig.credentials != null) { 167 | options['credentials'].addAll(contextConfig.credentials); 168 | } 169 | 170 | // the body depends on the http options 171 | final Map body = { 172 | 'operationName': operation.operationName, 173 | 'variables': operation.variables, 174 | }; 175 | 176 | // not sending the query (i.e persisted queries) 177 | if (http.includeExtensions) { 178 | body['extensions'] = operation.extensions; 179 | } 180 | 181 | if (http.includeQuery) { 182 | body['query'] = operation.document; 183 | } 184 | 185 | return HttpOptionsAndBody( 186 | options: options, 187 | body: json.encode(body), 188 | ); 189 | } 190 | 191 | FetchResult _parseResponse(Response response) { 192 | final int statusCode = response.statusCode; 193 | final String reasonPhrase = response.reasonPhrase; 194 | 195 | if (statusCode < 200 || statusCode >= 400) { 196 | throw ClientException( 197 | 'Network Error: $statusCode $reasonPhrase', 198 | ); 199 | } 200 | 201 | final Encoding encoding = _determineEncodingFromResponse(response); 202 | final dynamic decodedBody = encoding.decode(response.bodyBytes); 203 | 204 | final Map jsonResponse = json.decode(decodedBody); 205 | final FetchResult fetchResult = FetchResult(); 206 | 207 | if (jsonResponse['errors'] != null) { 208 | fetchResult.errors = jsonResponse['errors']; 209 | } 210 | 211 | if (jsonResponse['data'] != null) { 212 | fetchResult.data = jsonResponse['data']; 213 | } 214 | 215 | return fetchResult; 216 | } 217 | 218 | /// Returns the charset encoding for the given response. 219 | /// 220 | /// The default fallback encoding is set to UTF-8 according to the IETF RFC4627 standard 221 | /// which specifies the application/json media type: 222 | /// "JSON text SHALL be encoded in Unicode. The default encoding is UTF-8." 223 | Encoding _determineEncodingFromResponse(Response response, 224 | [Encoding fallback = utf8]) { 225 | final String contentType = response.headers['content-type']; 226 | 227 | if (contentType == null) { 228 | return fallback; 229 | } 230 | 231 | final MediaType mediaType = new MediaType.parse(contentType); 232 | final String charset = mediaType.parameters['charset']; 233 | 234 | if (charset == null) { 235 | return fallback; 236 | } 237 | 238 | final Encoding encoding = Encoding.getByName(charset); 239 | 240 | return encoding == null ? fallback : encoding; 241 | } 242 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.0-alpha.11] - October 28 2018 2 | 3 | ### Breaking changes 4 | 5 | n/a 6 | 7 | #### Fixes / Enhancements 8 | 9 | - Added `NormalizedInMemoryCache` as a new cache option. @micimize 10 | - Fixed `Mutation` calling `onCompleted` for loading state. @rafaelring 11 | - Fix type annotations. @HofmannZ 12 | - Fixed http versions. @HofmannZ 13 | 14 | #### Docs 15 | 16 | - Added docs for the new `NormalizedInMemoryCache` option. @micimize 17 | - Added @rafaelring as a contributor. @HofmannZ 18 | 19 | ## [1.0.0-alpha.10] - October 6 2018 20 | 21 | ### Breaking changes 22 | 23 | n/a 24 | 25 | #### Fixes / Enhancements 26 | 27 | - Fixed `Query` variables not updating in the query. @micimize 28 | - Fixed `Mutation` widget's behavior to properly set loading status. @Igor1201 29 | 30 | #### Docs 31 | 32 | - Added @micimize as a contributor. @HofmannZ 33 | - Added @Igor1201 as a contributor. @HofmannZ 34 | 35 | ## [1.0.0-alpha.9] - September 25 2018 36 | 37 | ### Breaking changes 38 | 39 | n/a 40 | 41 | #### Fixes / Enhancements 42 | 43 | - Fixed connectivity errors not being thrown and streamed. @HofmannZ 44 | 45 | #### Docs 46 | 47 | n/a 48 | 49 | ## [1.0.0-alpha.8] - September 21 2018 50 | 51 | ### Breaking changes 52 | 53 | n/a 54 | 55 | #### Fixes / Enhancements 56 | 57 | - Removed an unused class. @HofmannZ 58 | - Formatted the query manger. @HofmannZ 59 | - Handle charset encoding in responses @kolja-esders 60 | 61 | #### Docs 62 | 63 | - Added some inline docs to Query widget. @HofmannZ 64 | - Improved the inline docs of the client. @HofmannZ 65 | - Update the example. @HofmannZ 66 | 67 | ## [1.0.0-alpha.7] - September 14 2018 68 | 69 | ### Breaking changes 70 | 71 | n/a 72 | 73 | #### Fixes / Enhancements 74 | 75 | - Fixed a bug where getting the operation name was always returning null. @HofmannZ 76 | - Override the fetch policy if the default query option is used. @HofmannZ 77 | - Split up fetching and polling in the observable query. @HofmannZ 78 | - Check if the stream is closed, before adding a new event to it. @HofmannZ 79 | - Check if the variables have actually changed form or to null. @HofmannZ 80 | - Added a new getter to check if a query result has errors. @HofmannZ 81 | - Refactored the scheduler to only handle polling queries. @HofmannZ 82 | - Updated the mutation widget to use the new api in observable query. @HofmannZ 83 | - Resolve type cast exception when handling GraphQL errors. @kolja-esders @HofmannZ 84 | - Propagate GraphQL errors to caller instead of throwing network exception. @kolja-esders 85 | 86 | #### Docs 87 | 88 | n/a 89 | 90 | ## [1.0.0-alpha.6] - September 10 2018 91 | 92 | ### Breaking changes 93 | 94 | n/a 95 | 96 | #### Fixes / Enhancements 97 | 98 | - Updated lint options in preparation for upcoming CI checks. @HofmannZ 99 | 100 | #### Docs 101 | 102 | n/a 103 | 104 | ## [1.0.0-alpha.5] - September 7 2018 105 | 106 | ### Breaking changes 107 | 108 | n/a 109 | 110 | #### Fixes / Enhancements 111 | 112 | - Fixed a bug where the wrong key was selected from the context map. @HofmannZ 113 | - Fixed a scenario where the dispose method was calling the `close` method on the `observableQuery` class which might not have been initialised yet. @HofmannZ 114 | - Added the `onComplete` callback for the `Mutation` widget. @HofmannZ 115 | - Added the `initPayload` as an optional parameter for the `connect` method on the `SocketClient` class. @lordgreg 116 | 117 | #### Docs 118 | 119 | - Added an example of optionally overriding http options trough the context. @HofmannZ 120 | - Added @lordgreg as a contributor. @HofmannZ 121 | - Updated the example with explicit type casting. @HofmannZ 122 | - Updated the `Mutation` example with the new `onComplete` callback. @HofmannZ 123 | 124 | ## [1.0.0-alpha.4] - September 4 2018 125 | 126 | ### Breaking changes 127 | 128 | n/a 129 | 130 | #### Fixes / Enhancements 131 | 132 | - Always return something from the `read` method in the cache class. @HofmannZ 133 | - Only save to cache with certain fetch policies. @HofmannZ 134 | - Throw an error when no data from network with certain fetch policies. @HofmannZ 135 | - Added a document parser. @HofmannZ 136 | - Added operation name from document to the operation. @HofmannZ 137 | - Only create a new observable query if options have changed. @HofmannZ 138 | - Add context to the links. @HofmannZ 139 | - Parse context in the http link to update the config. @HofmannZ 140 | - Change the type of context from dynamic to Map=2.1.0-dev.0.0 <3.0.0` as recomended by Flutter `0.6.0`. @HofmannZ 172 | - Removed the old client from the library. @HofmannZ 173 | 174 | #### Docs 175 | 176 | - Document the new API. @HofmannZ 177 | - Write an upgrade guide. @HofmannZ 178 | - Clean up the example. @HofmannZ 179 | 180 | ## [1.0.0-alpha.1] - September 2 2018 181 | 182 | ### Breaking changes 183 | 184 | - Renamed `Client` to `GraphQLClient` to avoid name collision with other packages. @HofmannZ 185 | - Renamed `GraphqlProvider` to `GraphQLProvider` to align with new naming. @HofmannZ 186 | - Renamed `GraphqlConsumer` to `GraphQLConsumer` to align with new naming. @HofmannZ 187 | - Renamed `GQLError` to `GraphQLError` to align with new naming. @HofmannZ 188 | - `GraphQLClient` requires a `Link` to passed into the constructor. @HofmannZ 189 | - `GraphQLClient` no longer requires a `endPoint` or `apiToken` to be passed into the constructor. Instead you can provide it to the `Link`. @HofmannZ 190 | - The `Query` and `Mutation` widgets are now `StreamBuilders`, there the api did change slightly. @HofmannZ 191 | 192 | #### Fixes / Enhancements 193 | 194 | - Improved typing throughout the library. @HofmannZ 195 | - Queries are handled as streams of operations. @HofmannZ 196 | - Added the `HttpLink` to handle requests using http. @HofmannZ 197 | - `HttpLink` allows headers to be customised. @HofmannZ 198 | - The api allows contributors to write their own custom links. @HofmannZ 199 | 200 | #### Docs 201 | 202 | - Implement the new link system in the example. @HofmannZ 203 | 204 | ## [0.9.3] - September 5 2018 205 | 206 | ### Breaking changes 207 | 208 | n/a 209 | 210 | #### Fixes / Enhancements 211 | 212 | - Fix wrong typedef causing runtime type mismatch. @HofmannZ 213 | 214 | #### Docs 215 | 216 | - Update the reference to the next branch. @HofmannZ 217 | 218 | ## [0.9.2] - 2 September 2018 219 | 220 | ### Breaking changes 221 | 222 | n/a 223 | 224 | #### Fixes / Enhancements 225 | 226 | - Upgrade dependencies. @HofmannZ 227 | 228 | #### Docs 229 | 230 | - Added a refrence to our next major release. @HofmannZ 231 | 232 | ## [0.9.1] - August 30 2018 233 | 234 | ### Breaking changes 235 | 236 | n/a 237 | 238 | #### Fixes / Enhancements 239 | 240 | - Move test dependency to the dev section. @fabiocarneiro 241 | - Fix version resolving for test dependencies. @HofmannZ 242 | 243 | #### Docs 244 | 245 | n/a 246 | 247 | ## [0.9.0] - August 23 2018 248 | 249 | ### Breaking changes 250 | 251 | n/a 252 | 253 | #### Fixes / Enhancements 254 | 255 | - Added error extensions support. @dustin-graham 256 | - Changed the mutation typedef to return a Future, allowing async/await. @HofmannZ 257 | - Fixed error handling when location is not provided. @adelcasse 258 | - Fixed a bug where the client might no longer be in the same context. @HofmannZ 259 | 260 | #### Docs 261 | 262 | n/a 263 | 264 | ## [0.8.0] - August 10 2018 265 | 266 | ### Breaking changes 267 | 268 | n/a 269 | 270 | #### Fixes / Enhancements 271 | 272 | - Added basic error handeling for queries and mutations @mmadjer 273 | - Added missing export for the `GraphqlConsumer` widget @AleksandarFaraj 274 | 275 | #### Docs 276 | 277 | n/a 278 | 279 | ## [0.7.1] - August 3 2018 280 | 281 | ### Breaking changes 282 | 283 | n/a 284 | 285 | #### Fixes / Enhancements 286 | 287 | - Code formatting @HofmannZ 288 | 289 | #### Docs 290 | 291 | - Updated the package description @HofmannZ 292 | 293 | ## [0.7.0] - July 22 2018 294 | 295 | ### Breaking changes 296 | 297 | n/a 298 | 299 | #### Fixes / Enhancements 300 | 301 | - Added support for subsciptions in the client. @cal-pratt 302 | - Added the `Subscription` widget. You can no direcly acces streams from Flutter. @cal-pratt 303 | 304 | #### Docs 305 | 306 | - Added instructions for adding subscripton to your poject. @cal-pratt 307 | - Updated the `About this project` section. @HofmannZ 308 | 309 | ## [0.6.0] - July 19 2018 310 | 311 | ### Breaking changes 312 | 313 | - The library now requires your app to be wrapped with the `GraphqlProvider` widget. @HofmannZ 314 | - The global `client` variable is no longer available. Instead use the `GraphqlConsumer` widget. @HofmannZ 315 | 316 | #### Fixes / Enhancements 317 | 318 | - Added the `GraphqlProvider` widget. The client is now stored in an `InheritedWidget`, and can be accessed anywhere within the app. @HofmannZ 319 | 320 | ```dart 321 | Client client = GraphqlProvider.of(context).value; 322 | ``` 323 | 324 | - Added the `GraphqlConsumer` widget. For ease of use we added a widget that uses the same builder structure as the `Query` and `Mutation` widgets. @HofmannZ 325 | 326 | > Under the hood it access the client from the `BuildContext`. 327 | 328 | - Added the option to optionally provide the `apiToken` to the `Client` constructor. It is still possible to set the `apiToken` with setter method. @HofmannZ 329 | 330 | ```dart 331 | return new GraphqlConsumer( 332 | builder: (Client client) { 333 | // do something with the client 334 | 335 | return new Container(); 336 | }, 337 | ); 338 | ``` 339 | 340 | #### Docs 341 | 342 | - Added documentation for the new `GraphqlProvider` @HofmannZ 343 | - Added documentation for the new `GraphqlConsumer` @HofmannZ 344 | - Changed the setup instructions to include the new widgets @HofmannZ 345 | - Changed the example to include the new widgets @HofmannZ 346 | 347 | ## [0.5.4] - July 17 2018 348 | 349 | ### Breaking changes 350 | 351 | n/a 352 | 353 | #### Fixes / Enhancements 354 | 355 | - Query: changed `Timer` to `Timer.periodic` @eusdima 356 | - Minor logic tweak @eusdima 357 | - Use absolute paths in the library @HofmannZ 358 | 359 | #### Docs 360 | 361 | - Fix mutations example bug not updating star bool @cal-pratt 362 | 363 | ## [0.5.3] - July 13 2018 364 | 365 | ### Breaking changes 366 | 367 | n/a 368 | 369 | #### Fixes / Enhancements 370 | 371 | - Added polling timer as a variable for easy deletion on dispose 372 | - Fixed bug when Query timer is still active when the Query is disposed 373 | - Added instant query fetch when the query variables are updated 374 | 375 | #### Docs 376 | 377 | n/a 378 | 379 | ## [0.5.2] - July 11 2018 380 | 381 | ### Breaking changes 382 | 383 | n/a 384 | 385 | #### Fixes / Enhancements 386 | 387 | - Fixed error when cache file is non-existent 388 | 389 | #### Docs 390 | 391 | n/a 392 | 393 | ## [0.5.1] - June 29 2018 394 | 395 | ### Breaking changes 396 | 397 | n/a 398 | 399 | #### Fixes / Enhancements 400 | 401 | - Fixed json error parsing. 402 | 403 | #### Docs 404 | 405 | n/a 406 | 407 | ## [0.5.0] - June 25 2018 408 | 409 | ### Breaking changes 410 | 411 | n/a 412 | 413 | #### Fixes / Enhancements 414 | 415 | - Introduced `onCompleted` callback for mutiations. 416 | - Excluded some config files from version control. 417 | 418 | #### Docs 419 | 420 | - Fixed typos in the `readme.md`. 421 | - The examples inculde an example of the `onCompleted` callback. 422 | 423 | ## [0.4.1] - June 22 2018 424 | 425 | ### Breaking changes 426 | 427 | n/a 428 | 429 | #### Fixes / Enhancements 430 | 431 | n/a 432 | 433 | #### Docs 434 | 435 | - The examples now porperly reflect the changes to the library. 436 | 437 | ## [0.4.0] - June 21 2018 438 | 439 | ### Breaking changes 440 | 441 | - The Client now requires a from of cache. 442 | - The name of the `execute` method on the `Client` class changed to `query`. 443 | 444 | #### Fixes / Enhancements 445 | 446 | - Implemented in-memory cache. 447 | - Write memory to file when in background. 448 | - Added provider widget to save and restore the in-memory cache. 449 | - Restructure the project. 450 | 451 | #### Docs 452 | 453 | - Update the `README.md` to refelct changes in the code. 454 | - update the example to refelct changes in the code. 455 | 456 | ## [0.3.0] - June 16 2018 457 | 458 | ### Breaking changes 459 | 460 | - Changed data type to `Map` instaid of `Object` to be more explicit. 461 | 462 | #### Fixes / Enhancements 463 | 464 | - Cosmatic changes. 465 | 466 | #### Docs 467 | 468 | - Added a Flutter app example. 469 | - Fixed the example in `README.md`. 470 | - Added more badges. 471 | 472 | ## [0.2.0] - June 15 2018 473 | 474 | ### Breaking changes 475 | 476 | - Changed query widget `polling` argument to `pollInterval`, following the [react-apollo](https://github.com/apollographql/react-apollo) api. 477 | 478 | #### Fixes / Enhancements 479 | 480 | - Query polling is now optional. 481 | 482 | #### Docs 483 | 484 | - Updated the docs with the changes in api. 485 | 486 | ## [0.1.0] - June 15 2018 487 | 488 | My colleague and I created a simple implementation of a GraphQL Client for Flutter. (Many thanks to Eus Dima, for his work on the initial client.) 489 | 490 | ### Breaking changes 491 | 492 | n/a 493 | 494 | #### Fixes / Enhancements 495 | 496 | - A client to connect to your GraphQL server. 497 | - A query widget to handle GraphQL queries. 498 | - A mutation widget to handle GraphQL mutations. 499 | - Simple support for query polling. 500 | 501 | #### Docs 502 | 503 | - Initial documentation. 504 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter GraphQL 2 | 3 | [![version][version-badge]][package] 4 | [![MIT License][license-badge]][license] 5 | [![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors) 6 | [![PRs Welcome][prs-badge]](http://makeapullrequest.com) 7 | 8 | [![Watch on GitHub][github-watch-badge]][github-watch] 9 | [![Star on GitHub][github-star-badge]][github-star] 10 | 11 | ## NOTICE Project moved over here => https://github.com/snowballdigital/flutter-graphql From now on updates will be made here. 12 | 13 | ## Table of Contents 14 | 15 | - [Flutter GraphQL](#flutter-graphql) 16 | - [Table of Contents](#table-of-contents) 17 | - [About this project](#about-this-project) 18 | - [Installation](#installation) 19 | - [Usage](#usage) 20 | - [GraphQL Provider](#graphql-provider) 21 | - [Graphql Link and Headers] (#graphql-link-and-headers) 22 | - [Offline Cache](#offline-cache) 23 | - [Normalization](#normalization) 24 | - [Queries](#queries) 25 | - [Mutations](#mutations) 26 | - [Subscriptions (Experimental)](#subscriptions-experimental) 27 | - [Graphql Consumer](#graphql-consumer) 28 | - [Fragments](#fragments) 29 | - [Usage outside a widget](#outside-a-widget) 30 | - [Roadmap](#roadmap) 31 | - [Contributing](#contributing) 32 | - [New Contributors](#new-contributors) 33 | - [Founding Contributors](#founding-contributors) 34 | 35 | ## About this project 36 | 37 | GraphQL brings many benefits, both to the client: devices will need less requests, and therefore reduce data useage. And to the programer: requests are arguable, they have the same structure as the request. 38 | 39 | This project combines the benefits of GraphQL with the benefits of `Streams` in Dart to deliver a high performace client. 40 | 41 | The project took inspriation from the [Apollo GraphQL client](https://github.com/apollographql/apollo-client), great work guys! 42 | 43 | **Note: Still in Beta** 44 | **Docs is coming soon** 45 | **Support for all Apollo Graphql component supported props is coming soon** 46 | 47 | ## Installation 48 | 49 | First depend on the library by adding this to your packages `pubspec.yaml`: 50 | 51 | ```yaml 52 | dependencies: 53 | flutter_graphql: ^1.0.0-rc.3 54 | ``` 55 | 56 | Now inside your Dart code you can import it. 57 | 58 | ```dart 59 | import 'package:flutter_graphql/flutter_graphql.dart'; 60 | ``` 61 | 62 | ## Usage 63 | 64 | To use the client it first needs to be initialized with an link and cache. For this example we will be uing an `HttpLink` as our link and `InMemoryCache` as our cache. If your endpoint requires authentication you can provide some custom headers to `HttpLink`. 65 | 66 | > For this example we will use the public GitHub API. 67 | 68 | ```dart 69 | ... 70 | 71 | import 'package:flutter_graphql/flutter_graphql.dart'; 72 | 73 | void main() { 74 | HttpLink link = HttpLink( 75 | uri: 'https://api.github.com/graphql', 76 | headers: { 77 | 'Authorization': 'Bearer ', 78 | }, 79 | ); 80 | 81 | ValueNotifier client = ValueNotifier( 82 | GraphQLClient( 83 | cache: InMemoryCache(), 84 | link: link, 85 | ), 86 | ); 87 | 88 | ... 89 | } 90 | 91 | ... 92 | ``` 93 | 94 | ### GraphQL Provider 95 | 96 | In order to use the client, you `Query` and `Mutation` widgets to be wrapped with the `GraphQLProvider` widget. 97 | 98 | > We recommend wrapping your `MaterialApp` with the `GraphQLProvider` widget. 99 | 100 | ```dart 101 | ... 102 | 103 | return GraphQLProvider( 104 | client: client, 105 | child: MaterialApp( 106 | title: 'Flutter Demo', 107 | ... 108 | ), 109 | ); 110 | 111 | ... 112 | ``` 113 | 114 | ### Offline Cache 115 | 116 | The in-memory cache can automatically be saved to and restored from offline storage. Setting it up is as easy as wrapping your app with the `CacheProvider` widget. 117 | 118 | > It is required to place the `CacheProvider` widget is inside the `GraphQLProvider` widget, because `GraphQLProvider` makes client available trough the build context. 119 | 120 | ```dart 121 | ... 122 | 123 | class MyApp extends StatelessWidget { 124 | @override 125 | Widget build(BuildContext context) { 126 | return GraphQLProvider( 127 | client: client, 128 | child: CacheProvider( 129 | child: MaterialApp( 130 | title: 'Flutter Demo', 131 | ... 132 | ), 133 | ), 134 | ); 135 | } 136 | } 137 | 138 | ... 139 | ``` 140 | 141 | ### Graphql Link and Headers 142 | You can setup authentication headers and other custom links just like you do with Apollo Graphql 143 | 144 | ```dart 145 | import 'dart:async'; 146 | 147 | import 'package:flutter/material.dart'; 148 | import 'package:flutter_graphql/flutter_graphql.dart'; 149 | import 'package:flutter_graphql/src/link/operation.dart'; 150 | import 'package:flutter_graphql/src/link/fetch_result.dart'; 151 | 152 | class AuthLink extends Link { 153 | AuthLink() 154 | : super( 155 | request: (Operation operation, [NextLink forward]) { 156 | StreamController controller; 157 | 158 | Future onListen() async { 159 | try { 160 | var token = await AuthUtil.getToken(); 161 | operation.setContext(>{ 162 | 'headers': {'Authorization': '''bearer $token'''} 163 | }); 164 | } catch (error) { 165 | controller.addError(error); 166 | } 167 | 168 | await controller.addStream(forward(operation)); 169 | await controller.close(); 170 | } 171 | 172 | controller = StreamController(onListen: onListen); 173 | 174 | return controller.stream; 175 | }, 176 | ); 177 | } 178 | 179 | var cache = InMemoryCache(); 180 | 181 | var authLink = AuthLink() 182 | .concat(HttpLink(uri: 'http://yourgraphqlserver.com/graphql')); 183 | 184 | final ValueNotifier client = ValueNotifier( 185 | GraphQLClient( 186 | cache: cache, 187 | link: authLink, 188 | ), 189 | ); 190 | 191 | final ValueNotifier anotherClient = ValueNotifier( 192 | GraphQLClient( 193 | cache: cache, 194 | link: authLink, 195 | ), 196 | ); 197 | 198 | ``` 199 | However note that **`flutter-graphql` does not inject __typename into operations** the way apollo does, so if you aren't careful to request them in your query, this normalization scheme is not possible. 200 | 201 | #### Normalization 202 | To enable [apollo-like normalization](https://www.apollographql.com/docs/react/advanced/caching.html#normalization), use a `NormalizedInMemoryCache`: 203 | ```dart 204 | ValueNotifier client = ValueNotifier( 205 | GraphQLClient( 206 | cache: NormalizedInMemoryCache( 207 | dataIdFromObject: typenameDataIdFromObject, 208 | ), 209 | link: link, 210 | ), 211 | ); 212 | ``` 213 | `dataIdFromObject` is required and has no defaults. Our implementation is similar to apollo's, requiring a function to return a universally unique string or `null`. The predefined `typenameDataIdFromObject` we provide is similar to apollo's default: 214 | ```dart 215 | String typenameDataIdFromObject(Object object) { 216 | if (object is Map && 217 | object.containsKey('__typename') && 218 | object.containsKey('id')) { 219 | return "${object['__typename']}/${object['id']}"; 220 | } 221 | return null; 222 | } 223 | ``` 224 | However note that **`flutter-graphql` does not inject __typename into operations** the way apollo does, so if you aren't careful to request them in your query, this normalization scheme is not possible. 225 | 226 | 227 | ### Queries 228 | 229 | To create a query, you just need to define a String variable like the one below. With full support of fragments 230 | 231 | ```dart 232 | const GET_ALL_PEOPLE = ''' 233 | query getPeople{ 234 | readAll{ 235 | name 236 | age 237 | sex 238 | } 239 | } 240 | '''; 241 | ``` 242 | 243 | In your widget: 244 | 245 | ```dart 246 | ... 247 | 248 | Query( 249 | options: QueryOptions( 250 | document: GET_ALL_PEOPLE, // this is the query string you just created 251 | pollInterval: 10, 252 | ), 253 | builder: (QueryResult result) { 254 | if (result.errors != null) { 255 | return Text(result.errors.toString()); 256 | } 257 | 258 | if (result.loading) { 259 | return Text('Loading'); 260 | } 261 | 262 | // it can be either Map or List 263 | List people = result.data['getPeople']; 264 | 265 | return ListView.builder( 266 | itemCount: people.length, 267 | itemBuilder: (context, index) { 268 | final repository = people[index]; 269 | 270 | return Text(people['name']); 271 | }); 272 | }, 273 | ); 274 | 275 | ... 276 | ``` 277 | 278 | Other examples with query argments and passing in a custom graphql client 279 | 280 | ```dart 281 | const READ_BY_ID = ''' 282 | query readById(\$id: String!){ 283 | readById(ID: \$id){ 284 | name 285 | age 286 | sex 287 | } 288 | } 289 | 290 | 291 | final ValueNotifier userClient = ValueNotifier( 292 | GraphQLClient( 293 | cache: cache, 294 | link: authLinkProfile, 295 | ), 296 | ); 297 | 298 | '''; 299 | ``` 300 | 301 | In your widget: 302 | 303 | ```dart 304 | ... 305 | 306 | Query( 307 | options: QueryOptions( 308 | document: READ_BY_ID, // this is the query string you just created 309 | pollInterval: 10, 310 | client: userClient.value 311 | ), 312 | builder: (QueryResult result) { 313 | if (result.errors != null) { 314 | return Text(result.errors.toString()); 315 | } 316 | 317 | if (result.loading) { 318 | return Text('Loading'); 319 | } 320 | 321 | // it can be either Map or List 322 | List person = result.data['getPeople']; 323 | 324 | return Text(person['name']); 325 | }, 326 | ); 327 | 328 | ... 329 | ``` 330 | 331 | ### Mutations 332 | 333 | Again first create a mutation string: 334 | 335 | ```dart 336 | const LIKE_BLOG = ''' 337 | mutation likeBlog(\$id: Int!) { 338 | likeBlog(id: \$id){ 339 | name 340 | author { 341 | name 342 | displayImage 343 | } 344 | } 345 | '''; 346 | ``` 347 | 348 | The syntax for mutations is fairly similar to that of a query. The only diffence is that the first argument of the builder function is a mutation function. Just call it to trigger the mutations (Yeah we deliberately stole this from react-apollo.) 349 | 350 | ```dart 351 | ... 352 | 353 | Mutation( 354 | options: MutationOptions( 355 | document: LIKE_BLOG, // this is the mutation string you just created 356 | ), 357 | builder: ( 358 | RunMutation runMutation, 359 | QueryResult result, 360 | ) { 361 | return FloatingActionButton( 362 | onPressed: () => runMutation({ 363 | 'id': , 364 | }), 365 | tooltip: 'Star', 366 | child: Icon(Icons.star), 367 | ); 368 | }, 369 | ); 370 | 371 | ... 372 | ``` 373 | 374 | ### Subscriptions (Experimental) 375 | 376 | The syntax for subscriptions is again similar to a query, however, this utilizes WebSockets and dart Streams to provide real-time updates from a server. 377 | Before subscriptions can be performed a global intance of `socketClient` needs to be initialized. 378 | 379 | > We are working on moving this into the same `GraphQLProvider` stucture as the http client. Therefore this api might change in the near future. 380 | 381 | ```dart 382 | socketClient = await SocketClient.connect('ws://coolserver.com/graphql'); 383 | ``` 384 | 385 | Once the `socketClient` has been initialized it can be used by the `Subscription` `Widget` 386 | 387 | ```dart 388 | class _MyHomePageState extends State { 389 | @override 390 | Widget build(BuildContext context) { 391 | return Scaffold( 392 | body: Center( 393 | child: Subscription( 394 | operationName, 395 | query, 396 | variables: variables, 397 | builder: ({ 398 | bool loading, 399 | dynamic payload, 400 | dynamic error, 401 | }) { 402 | if (payload != null) { 403 | return Text(payload['requestSubscription']['requestData']); 404 | } else { 405 | return Text('Data not found'); 406 | } 407 | } 408 | ), 409 | ) 410 | ); 411 | } 412 | } 413 | ``` 414 | 415 | ### Graphql Consumer 416 | 417 | You can always access the client direcly from the `GraphQLProvider` but to make it even easier you can also use the `GraphQLConsumer` widget. You can also pass in a another client to the consumer 418 | 419 | ```dart 420 | ... 421 | 422 | return GraphQLConsumer( 423 | builder: (GraphQLClient client) { 424 | // do something with the client 425 | 426 | return Container( 427 | child: Text('Hello world'), 428 | ); 429 | }, 430 | ); 431 | 432 | ... 433 | ``` 434 | 435 | A different client: 436 | 437 | ```dart 438 | ... 439 | 440 | return GraphQLConsumer( 441 | client: userClient, 442 | builder: (GraphQLClient client) { 443 | // do something with the client 444 | 445 | return Container( 446 | child: Text('Hello world'), 447 | ); 448 | }, 449 | ); 450 | 451 | ... 452 | ``` 453 | 454 | ### Fragments 455 | 456 | There is support for fragments and it's basically how you use it in Apollo React. For example define your fragment as a dart String. 457 | 458 | ```dart 459 | ... 460 | const UserFragment = ''' 461 | fragment UserFragmentFull on Profile { 462 | address { 463 | city 464 | country 465 | postalCode 466 | street 467 | } 468 | birthdate 469 | email 470 | firstname 471 | id 472 | lastname] 473 | } 474 | '''; 475 | 476 | ... 477 | ``` 478 | 479 | Now you can use it in your Graphql Query or Mutation String like below 480 | ```dart 481 | ... 482 | 483 | const CURRENT_USER = ''' 484 | query read{ 485 | read { 486 | ...UserFragmentFull 487 | } 488 | } 489 | $UserFragment 490 | '''; 491 | 492 | ... 493 | ``` 494 | 495 | or 496 | 497 | ```dart 498 | ... 499 | 500 | const GET_BLOGS = ''' 501 | query getBlogs{ 502 | getBlog { 503 | title 504 | description 505 | tags 506 | 507 | author { 508 | ...UserFragmentFull 509 | } 510 | } 511 | $UserFragment 512 | '''; 513 | 514 | ... 515 | ``` 516 | 517 | ### Outside a Widget 518 | 519 | Similar to withApollo or graphql HoC that passes the client to the component in react, you can call a graphql query from any part of your code base even in a your service class or in your Scoped MOdel or Bloc class. Example 520 | 521 | ```dart 522 | ... 523 | 524 | class AuthUtil{ 525 | static Future getToken() async { 526 | SharedPreferences prefs = await SharedPreferences.getInstance(); 527 | return await prefs.getString('token'); 528 | } 529 | 530 | static Future setToken(value) async { 531 | SharedPreferences prefs = await SharedPreferences.getInstance(); 532 | return await prefs.setString('token', value); 533 | } 534 | 535 | static removeToken() async { 536 | SharedPreferences prefs = await SharedPreferences.getInstance(); 537 | return await prefs.remove('token'); 538 | } 539 | 540 | static clear() async { 541 | SharedPreferences prefs = await SharedPreferences.getInstance(); 542 | return await prefs.clear(); 543 | } 544 | 545 | static Future logIn(String username, String password) async { 546 | var token; 547 | 548 | QueryOptions queryOptions = QueryOptions( 549 | document: LOGIN, 550 | variables: { 551 | 'username': username, 552 | 'password': password 553 | } 554 | ); 555 | 556 | if (result != null) { 557 | this.setToken(result); 558 | return clientProfile.value.query(queryOptions).then((result) async { 559 | 560 | if(result.data != null) { 561 | token = result.data['login']['token]; 562 | notifyListeners(); 563 | return token; 564 | } else { 565 | return throw Error; 566 | } 567 | 568 | }).catchError((error) { 569 | return throw Error; 570 | }); 571 | } else 572 | return false; 573 | } 574 | } 575 | 576 | ... 577 | ``` 578 | 579 | In a scoped model: 580 | 581 | ```dart 582 | ... 583 | class AppModel extends Model { 584 | 585 | String token = ''; 586 | var currentUser = new Map (); 587 | 588 | static AppModel of(BuildContext context) => 589 | ScopedModel.of(context); 590 | 591 | void setToken(String value) { 592 | token = value; 593 | AuthUtil.setAppURI(value); 594 | notifyListeners(); 595 | } 596 | 597 | 598 | String getToken() { 599 | if (token != null) return token; 600 | else AuthUtil.getToken(); 601 | } 602 | 603 | getCurrentUser() { 604 | return currentUser; 605 | } 606 | 607 | Future isLoggedIn() async { 608 | 609 | var result = await AuthUtil.getToken(); 610 | print(result); 611 | 612 | QueryOptions queryOptions = QueryOptions( 613 | document: CURRENT_USER 614 | ); 615 | 616 | if (result != null) { 617 | print(result); 618 | this.setToken(result); 619 | return clientProfile.value.query(queryOptions).then((result) async { 620 | 621 | if(result.data != null) { 622 | currentUser = result.data['read']; 623 | notifyListeners(); 624 | return true; 625 | } else { 626 | return false; 627 | } 628 | 629 | }).catchError((error) { 630 | print('''Error => $error'''); 631 | return false; 632 | }); 633 | } else 634 | return false; 635 | } 636 | } 637 | ``` 638 | 639 | ## Roadmap 640 | 641 | This is currently our roadmap, please feel free to request additions/changes. 642 | 643 | | Feature | Progress | 644 | | :---------------------- | :------: | 645 | | Queries | ✅ | 646 | | Mutations | ✅ | 647 | | Subscriptions | ✅ | 648 | | Query polling | ✅ | 649 | | In memory cache | ✅ | 650 | | Offline cache sync | ✅ | 651 | | Optimistic results | 🔜 | 652 | | Client state management | 🔜 | 653 | | Modularity | 🔜 | 654 | | Documentation | 🔜 | 655 | 656 | ## Contributing 657 | 658 | Feel free to open a PR with any suggestions! We'll be actively working on the library ourselves. If you need control to the repo, please contact me Rex Raphael. Please fork and send your PRs to the v.1.0.0 branch. 659 | 660 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind are welcome! 661 | 662 | [version-badge]: https://img.shields.io/pub/v/flutter_graphql.svg?style=flat-square 663 | [package]: https://pub.dartlang.org/packages/flutter_graphql/versions/1.0.0-alpha.12 664 | [license-badge]: https://img.shields.io/github/license/juicycleff/flutter-graphql.svg?style=flat-square 665 | [license]: https://github.com/juicycleff/flutter-graphql/blob/master/LICENSE 666 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 667 | [prs]: http://makeapullrequest.com 668 | [github-watch-badge]: https://img.shields.io/github/watchers/juicycleff/flutter-graphql.svg?style=social 669 | [github-watch]: https://github.com/juicycleff/flutter-graphql/watchers 670 | [github-star-badge]: https://img.shields.io/github/stars/juicycleff/flutter-graphql.svg?style=social 671 | [github-star]: https://github.com/juicycleff/flutter-graphql/stargazers 672 | --------------------------------------------------------------------------------