From bac82a3fc44724da9085ac45ed78d0570b1cbcf8 Mon Sep 17 00:00:00 2001 From: Martin Kamleithner Date: Sun, 7 Apr 2024 15:46:34 +0100 Subject: [PATCH] docs(ferry): improve documentation around accessing plugins from isolates since Flutter 3.7 (#591) --- docs/isolates.md | 130 +++++++++++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 43 deletions(-) diff --git a/docs/isolates.md b/docs/isolates.md index f329e4f7..1286ba47 100644 --- a/docs/isolates.md +++ b/docs/isolates.md @@ -4,69 +4,111 @@ 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. +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! +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 +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. +as all communication over isolates is asynchronous. ### Setup To run Ferry on a separate Isolate, use the static `IsolateClient.create()` method. This method receives three parameters and one generic type parameter: -- ``: 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` here. +- ``: this generic type parameter defines the type of the parameters object which is + used + to initialize the client. If you don't want to write a separate class for this, you can just + use `Map` 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 +- `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 +#### No MethodChannels in background isolates before Flutter 3.7 + +:::note + +As of Flutter 3.7, Flutter supports platform plugins in background isolates. +See https://docs.flutter.dev/perf/isolates#using-platform-plugins-in-isolates + +This simplifies the setup for Ferry in isolates, and invalidates much of the information below. + +Beware that if you write to `SharedPreferences` in the background isolate, +you might need to call `.reload()` on the SharedPreferences instance +in the main isolate to see the changes. + +::: + +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: -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. +- 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: +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. +- 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 @@ -78,7 +120,9 @@ If you implement another approach, feel free to send me a sample code so I can a ### updateResult / pagination -If you set `updateResult` paremeter in queries with the `IsolateClient`, you need to make sure that the `updateResult` function -can be sent to the ferry isolate. The easiest way to do ensure this to make it a top-level or static function. +If you set `updateResult` parameter in queries with the `IsolateClient`, you need to make sure that +the `updateResult` function +can be sent to the ferry isolate. The easiest way to do ensure this to make it a top-level or static +function. The refetch a request, call the `addRequestToRequestController` method on the `IsolateClient`. \ No newline at end of file