Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ferry): add IsolateClient #405

Merged
merged 21 commits into from
Sep 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions examples/pokemon_explorer/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import 'package:ferry/ferry.dart';
import './src/client.dart';
import './src/app.dart';

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

void main() async {
WidgetsFlutterBinding.ensureInitialized();
final client = await initIsolateClient();
GetIt.I.registerLazySingleton<IsolateClient>(() => client);
final client = await initClient();
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());
}
19 changes: 5 additions & 14 deletions examples/pokemon_explorer/lib/src/client.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import 'package:ferry/ferry_isolate.dart';
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:path_provider/path_provider.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({Map<String, dynamic>? params}) async {
Hive.init(params!["hivePath"]);
Future<Client> initClient() async {
await Hive.initFlutter();

final box = await Hive.openBox<Map<String, dynamic>>("graphql");

Expand All @@ -17,7 +15,7 @@ Future<Client> initClient({Map<String, dynamic>? params}) async {

final cache = Cache(store: store);

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

final client = Client(
link: link,
Expand All @@ -26,10 +24,3 @@ Future<Client> initClient({Map<String, dynamic>? params}) async {

return client;
}

Future<IsolateClient> initIsolateClient() async {
final client = IsolateClient.spawn(initClient,
params: {'hivePath': (await getApplicationDocumentsDirectory()).path});

return client;
}
46 changes: 46 additions & 0 deletions examples/pokemon_explorer/lib/src/client_isolate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'dart:isolate';
import 'dart:ui';

import 'package:ferry/ferry.dart';
import 'package:ferry/ferry_isolate.dart';
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pokemon_explorer/main.dart';

Future<IsolateClient> initIsolateClient() async {
final client = IsolateClient.create(_initClientIsolate, params: {
'hivePath': (await getApplicationDocumentsDirectory()).path,
'url': apiUrl,
});

return client;
}

// this is called on the new isolate
// if you passed a messageHandler to the IsolateClient, you can use the sendPort
// to send arbitrary messages to the main isolate.
Future<Client> _initClientIsolate(Map<String, dynamic>? params, SendPort? sendPort) async {
// don't use Hive.initFlutter to avoid dealing with method channels in the isolate
// instead, call getApplicationDocumentsDirectory() on the main isolate
// and pass the result to the ferry isolate
Hive.init(params!["hivePath"]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may as well make params required if we expect a hivePath and a url.
Actually, it should be a class rather than a Map.

Copy link
Collaborator Author

@knaeckeKami knaeckeKami Sep 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to Map to be non-nullable now.

Good point. Currently, the signature of the function that creates a the client on the new Isolate must be

typedef InitClient = Future<TypedLinkWithCache> Function(
  Map<String, dynamic> params,
  SendPort? sendMessageToMessageHandler,
);

do you think we should make it generic, like

typedef InitClient<InitParams> = Future<TypedLinkWithCache> Function(
  InitParmams params,
  SendPort? sendMessageToMessageHandler,
);

and change the create() method of the IsolateClient to

 static Future<IsolateClient> create<InitParams>(InitClient<InitParams> initClient,
      {InitParams params,
      void Function(Object?)? messageHandler,
      IsolateSpawn? isolateSpawn})

This would be typesafe, which is definitely a big plus.

However, it could lead to users trying the send objects which cannot be serialized over isolates (like SharedPreferences, HiveBoxes, Closures... basically anything that is not just data), while Map<String, dynamic> is already typically used for json-like data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, Isolate.spawn is generic so I'd say it's fair to do the same.
We can always add an API comment with /// to say "Make sure InitParams is serializable as it will be passed to the isolate".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I'll update it.


final box = await Hive.openBox<Map<String, dynamic>>("graphql");

await box.clear();

final store = HiveStore(box);

final cache = Cache(store: store);

final link = HttpLink(params["url"]);

final client = Client(
link: link,
cache: cache,
);

return client;
}
10 changes: 7 additions & 3 deletions examples/pokemon_explorer/lib/src/pokemon_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ class PokemonCard extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: () => Navigator.of(context)
.pushNamed('detail', arguments: {'id': pokemon.id}),
onTap: () => Navigator.of(context).pushNamed('detail', arguments: {'id': pokemon.id}),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
SizedBox(
child: Ink.image(image: NetworkImage(pokemon.avatar)),
height: 200,
width: 200,
child: Image.network(
pokemon.avatar,
errorBuilder: (context, error, stacktrace) {
return Text("error loading image ${pokemon.avatar}: $error");
},
),
),
Text(
pokemon.name,
Expand Down
2 changes: 1 addition & 1 deletion examples/pokemon_explorer/lib/src/pokemon_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import './graphql/__generated__/pokemon_detail.var.gql.dart';
import './pokemon_card.dart';

class PokemonDetailScreen extends StatelessWidget {
final client = GetIt.I<IsolateClient>();
final client = GetIt.I<TypedLink>();

final int id;

Expand Down
2 changes: 1 addition & 1 deletion examples/pokemon_explorer/lib/src/pokemon_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import './graphql/__generated__/all_pokemon.var.gql.dart';
import './pokemon_card.dart';

class PokemonListScreen extends StatelessWidget {
final client = GetIt.I<IsolateClient>();
final client = GetIt.I<TypedLink>();

@override
Widget build(BuildContext context) {
Expand Down
1 change: 1 addition & 0 deletions examples/pokemon_explorer/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies:
ferry_hive_store: ^0.4.5
ferry_flutter: ^0.6.1+1
hive: ^2.0.0
hive_flutter: ^1.1.0
get_it: ^7.1.3
path_provider: ^2.0.0
flutter:
Expand Down
8 changes: 6 additions & 2 deletions packages/ferry/lib/ferry.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:async';
import 'package:ferry_exec/ferry_exec.dart';
import 'package:gql/ast.dart';

import 'package:ferry/src/add_typename_typed_link.dart';
Expand All @@ -15,13 +14,18 @@ export 'package:ferry/src/update_cache_typed_link.dart' show UpdateCacheHandler;
export 'package:ferry_exec/ferry_exec.dart';
export 'package:gql/ast.dart' show OperationType;

class Client extends TypedLink {
abstract class TypedLinkWithCache extends TypedLink {
Cache get cache;
}

class Client extends TypedLinkWithCache {
final Link link;
final Map<String, TypePolicy> typePolicies;
final Map<String, Function> updateCacheHandlers;
final Map<OperationType, FetchPolicy> defaultFetchPolicies;
final bool addTypename;

@override
late Cache cache;
Cache? _defaultCache;

Expand Down
Loading