Skip to content

Commit

Permalink
feat(ferry): add IsolateClient (#405)
Browse files Browse the repository at this point in the history
* feat(ferry): add IsolateClient

* chore(pokemon_explorer): format

* feat(ferry): add more methods to IsolateClient

* feat(ferry): refactor IsolateClient, add more commands

* feat(ferry): more IsolateClient features

* chore(ferry): format

* feat(ferry): refactor single response commands

* docs(ferry): add documentation for IsolateClient in website

* test(ferry): add tests

* chore(ferry): format

* refactor(ferry): cleanup request response communication

* chore(ferry): format

* refactor(ferry): refactore handleCommand

* refactor(ferry): refactore handleCommand

* docs(ferry): add example for token refresh over isolate

* refactor(ferry): make params map non null in pokemon example

* chore(ferry): format

* chore(ferry): remove ignored lint, add unawaited

* refactor(ferry): use a generic type parameter for the init params instead of a Map<String,dynamic>

* docs(ferry): mention isolates in README

* chore(release): publish packages

 - ferry@0.11.1
 - ferry_exec@0.1.6
 - ferry_flutter@0.6.1+2
 - ferry_generator@0.6.0+1
 - ferry_cache@0.6.2+1
 - ferry_test_graphql@0.1.5-dev.4

Co-authored-by: Martin Kamleithner <martin+kamleithner@diagnosia.com>
  • Loading branch information
knaeckeKami and Martin Kamleithner committed Sep 18, 2022
1 parent 21c80b5 commit 35a434c
Show file tree
Hide file tree
Showing 44 changed files with 2,572 additions and 129 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ While we are using this package in our own production applications, we don't hav
5. 💾 **Multiple Data Stores**: extensible `Store` interface with built-in `MemoryStore` and `HiveStore` (which uses `hive` for offline persistence).
6. 📄 **Refetch & Pagination**: easily update responses with new data or combine multiple responses, allowing for seamless pagination.
7. 📱 **Flutter Widgets**: Widgets for Queries, Mutations, and Subscriptions, available out of the box.
8. 🧈 **Smooth** Run queries in a separate Isolate to free up resources on the UI thread

# Packages

Expand Down
77 changes: 77 additions & 0 deletions docs/isolates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
id: isolates
title: Running the client in a separate isolate
sidebar_label: Isolates
---

## Running the client in a separate isolate

The default setup should be sufficient for most use cases.
However, normalization and denormalization of big, nested responses can be quite computation heavy
and can lead to dropped frames on frontends.

Ferry can help you run your graphql-related code on a separate isolate so that the UI thread does
not get blocked when executing big queries.

Since the Ferry `Client` already is Stream based, the differences in the API between the default `Client` and the `IsolateClient` are minimal.
If you are just using the `request()` method of the `Client` or `Operation` widgets of ferry_flutter,
then the `IsolateClient` is a drop-in replacement!

The `IsolateClient` does not, however, offer direct access to the `Cache` object, but instead offers
indirect access via methods like `readQuery()` or `writeFragment()`. These methods are asynchronous,
as all communication over isolates is asynchrounous.

### Setup

To run Ferry on a separate Isolate, use the static `IsolateClient.create<InitParams>()` method.
This method receives three parameters and one generic type parameter:

- `<InitParams>`: this generic type parameter defines the type of the parameters object which is used
to initialze the client. If you don't want to write a separate class for this, you can just use `Map<String, dynamic>` here.
If you don't need parameters to initialize the Client, you can also use the `Null` type
- `initClient`: This function is called on a separate isolate and is responsible for creating the real Ferry `Client`.
This must be a top-level or static function (otherwise it will not be possible to send it to another isolate).
When migrating from the standard setup to to isolate-based setup, move the initialization of the `Client` class
to this function. initClient is also called with a `SendPort` parameter, which can be used to establish custom
communication between the two isolates. You can use this for example for synchronizing authentication tokens when
the are refreshed.
- `params`: a type that contains the parameters used to initialize the client, which will be passed to `initClient`.
Use this to pass endpoint urls, the path to the cache or a authentication token to the other isolate.
- `messageHandler` (optional): a function which will be invoked on the main isolate if you send objects
through the `SendPort` passed to `initClient`. If you want to establish two-way communication, create a new `ReceivePort`
in `InitClient` and send its `SendPort` over the `SendPort` which is the third parameter of `initClient`. `messageHandler` will
be called with the `sendPort` and this can be used to send custom messages from the main isolate the the ferry isolate.

A example can be found in `examples/pokemon_explorer`. In this project. The same App can be run with ferry
on the main isolate (`main.dart`) or a separate isolate (`main_isolate.dart`).

### Caveats

By default, you will not be able to run code that uses `MethodChannel`s underneath in the new isolate.
This means:
- no SharedPreferences
- no Hive.initFlutter (Hive.init works, though, also in flutter apps.)
- no path_provider

If you want to use Hive for the cache, there is a workaround implemented in the pokemon_explorer example app:
- call ` (await getApplicationDocumentsDirectory()).path` on the main isolate and pass the path to the ferry isolate in the `params` map
- use `Hive.init` with the given path instead of `Hive.initFlutter()`. Note that Hive is single threaded and you cannot use the same box on multiple isolates, this would lead to data corruption.

If you have an authenticated graphql api and need the auth token on both the main isolate and the ferry isolate, consider one of the following solutions:

- use a persistence library that can be used across different isolates like `drift` or `isar`.
- use a persistence library like `hive`, which does not support multiple isolate, can be used on non-main isolates also. However,
With this approach, the Hive box needs to be opened on the ferry isolate only, the main isolate will not be able the read the auth token.
- use the `SendPort` in the `InitClient` function that runs on the ferry isolate to establish communication between
the main isolate and ferry. For example you can send the new authentication token via that sendPort.
The main isolate would receive it in its `messageHandler` and could persist it, for example via `SharedPreferences`.
You can also establish a two-way communication be creating a `ReceivePort` in the `InitClient` function and send its sendport to
the main isolates messagehandler.

Here's an example on how to wire up SharedPreferences to store
the auth token on the main isolate, refresh it on the ferry isolate when needed, and
send the new token the the main isolate for shared_preferences to store:

https://gist.github.com/knaeckeKami/b11ad83e4b69aa44638815d1471c2ba3

If you implement another approach, feel free to send me a sample code so I can add it here.
2 changes: 1 addition & 1 deletion docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ module.exports = {
Fetching: ['queries', 'mutations', 'fetch-policies', 'pagination', 'error-handling'],
Caching: ['cache-configuration', 'cache-interaction', 'garbage-collection'],
Flutter: ['flutter', 'flutter-operation-widget', 'structuring-queries'],
Advanced: ['custom-scalars', 'customization'],
Advanced: ['custom-scalars', 'customization', 'isolates'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion examples/pokemon_explorer/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# platform :ios, '11.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
22 changes: 22 additions & 0 deletions examples/pokemon_explorer/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
PODS:
- Flutter (1.0.0)
- path_provider_ios (0.0.1):
- Flutter

DEPENDENCIES:
- Flutter (from `Flutter`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)

EXTERNAL SOURCES:
Flutter:
:path: Flutter
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"

SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02

PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3

COCOAPODS: 1.11.3
78 changes: 74 additions & 4 deletions examples/pokemon_explorer/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand All @@ -13,6 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
CC7D3E6E13B57FA2DD3C2503 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65560DD81A1547C7770B1DC9 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -32,23 +33,28 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
61130A17C404B949E0F8F11D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
65560DD81A1547C7770B1DC9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7DBA07462A78183BF9D45D75 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B9AA83C7A7E561745A36EB02 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CC7D3E6E13B57FA2DD3C2503 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -72,6 +78,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
BEC8FF9787E9A48278CDF5FF /* Pods */,
CE35EC18E8A7FB7A02D2E893 /* Frameworks */,
);
sourceTree = "<group>";
};
Expand All @@ -98,19 +106,40 @@
path = Runner;
sourceTree = "<group>";
};
BEC8FF9787E9A48278CDF5FF /* Pods */ = {
isa = PBXGroup;
children = (
61130A17C404B949E0F8F11D /* Pods-Runner.debug.xcconfig */,
B9AA83C7A7E561745A36EB02 /* Pods-Runner.release.xcconfig */,
7DBA07462A78183BF9D45D75 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
CE35EC18E8A7FB7A02D2E893 /* Frameworks */ = {
isa = PBXGroup;
children = (
65560DD81A1547C7770B1DC9 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
309A75A8BB1F500BC2FABAB3 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D6F9C35139DFF1C78B678633 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -169,8 +198,31 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
309A75A8BB1F500BC2FABAB3 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand All @@ -185,6 +237,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand All @@ -197,6 +250,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
D6F9C35139DFF1C78B678633 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
Expand Down Expand Up @@ -272,7 +342,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -349,7 +419,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -398,7 +468,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions examples/pokemon_explorer/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
5 changes: 4 additions & 1 deletion examples/pokemon_explorer/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import 'package:ferry/ferry_isolate.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:ferry/ferry.dart';

import './src/client.dart';
import './src/app.dart';

const apiUrl = "https://pokeapi.dev";

void main() async {
final client = await initClient();
GetIt.I.registerLazySingleton<Client>(() => client);
GetIt.I.registerLazySingleton<TypedLink>(() => client);
runApp(App());
}
14 changes: 14 additions & 0 deletions examples/pokemon_explorer/lib/main_isolate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:ferry/ferry.dart';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:pokemon_explorer/src/app.dart';

import 'src/client_isolate.dart';

// example for running the client in a separate isolate
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final client = await initIsolateClient();
GetIt.I.registerLazySingleton<TypedLink>(() => client);
runApp(App());
}
7 changes: 3 additions & 4 deletions examples/pokemon_explorer/lib/src/client.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import 'package:gql_http_link/gql_http_link.dart';
import 'package:ferry/ferry.dart';
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:pokemon_explorer/__generated__/schema.schema.gql.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:pokemon_explorer/main.dart';

Future<Client> initClient() async {
await Hive.initFlutter();
Expand All @@ -16,7 +15,7 @@ Future<Client> initClient() async {

final cache = Cache(store: store);

final link = HttpLink("https://pokeapi.dev");
final link = HttpLink(apiUrl);

final client = Client(
link: link,
Expand Down
Loading

0 comments on commit 35a434c

Please sign in to comment.