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 asynchronous.
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 initialize the client. If you don't want to write a separate class for this, you can just useMap<String, dynamic>
here. If you don't need parameters to initialize the Client, you can also use theNull
typeinitClient
: This function is called on a separate isolate and is responsible for creating the real FerryClient
. 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 theClient
class to this function. initClient is also called with aSendPort
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 toinitClient
. 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 theSendPort
passed toinitClient
. If you want to establish two-way communication, create a newReceivePort
inInitClient
and send itsSendPort
over theSendPort
which is the third parameter ofinitClient
.messageHandler
will be called with thesendPort
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
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:
- call
(await getApplicationDocumentsDirectory()).path
on the main isolate and pass the path to the ferry isolate in theparams
map - use
Hive.init
with the given path instead ofHive.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
orisar
. - 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 theInitClient
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 itsmessageHandler
and could persist it, for example viaSharedPreferences
. You can also establish a two-way communication be creating aReceivePort
in theInitClient
function and send itsSendPort
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.
updateResult / pagination
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
.