Skip to content

Commit

Permalink
Merge pull request #7 from zino-app/cache-1
Browse files Browse the repository at this point in the history
Implement in-memory cache and offline caching.
  • Loading branch information
HofmannZ authored Jun 22, 2018
2 parents 3818ac6 + 5c137a8 commit c9687ca
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 47 deletions.
3 changes: 2 additions & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"code",
"doc",
"example",
"ideas"
"ideas",
"review"
]
},
{
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.vs/

.DS_Store
.dart_tool/

Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
## [0.4.0] - June 21 2018

### Breaking change

- The Client now requires a from of cache.
- The name of the `execute` method on the `Client` class changed to `query`.

#### Fixes / Enhancements

- Implemented in-memory cache.
- Write memory to file when in background.
- Added provider widget to save and restore the in-memory cache.
- Restructure the project.

#### Docs

- Update the `README.md` to refelct changes in the code.
- update the example to refelct changes in the code.

## [0.3.0] - June 16 2018

### Breaking change
Expand Down
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Usage](#usage)
- [Queries](#queries)
- [Mutations](#mutations)
- [Offline Cache](#offline-cache)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [Contributors](#contributors)
Expand All @@ -24,7 +25,7 @@ First depend on the library by adding this to your packages `pubspec.yaml`:

```yaml
dependencies:
graphql_flutter: ^0.3.0
graphql_flutter: ^0.4.0
```
Now inside your Dart code you can import it.
Expand All @@ -35,7 +36,7 @@ import 'package:graphql_flutter/graphql_flutter.dart';

## Usage

To use the client it first needs to be initialzed with an endpoint. If your endpoint requires authentication you can provide it to the client by calling the setter `apiToken` on the `Client` class.
To use the client it first needs to be initialzed with an endpoint and cache. If your endpoint requires authentication you can provide it to the client by calling the setter `apiToken` on the `Client` class.

> For this example we will use the public GitHub API.
Expand All @@ -45,7 +46,10 @@ To use the client it first needs to be initialzed with an endpoint. If your endp
import 'package:graphql_flutter/graphql_flutter.dart';
void main() async {
client = new Client('https://api.github.com/graphql');
client = new Client(
endPoint: 'https://api.github.com/graphql',
cache: new InMemoryCache(), // currently the only cache type we have implemented.
);
client.apiToken = '<YOUR_GITHUB_PERSONAL_ACCESS_TOKEN>';
...
Expand Down Expand Up @@ -157,6 +161,31 @@ new Mutation(
...
```

## Offline Cache

The in-memory cache can autmaticly be saved to and restored from offline storage. Setting it up is as easy as wrapping your app with the `CacheProvider` widget.

```dart
...
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new CacheProvider(
child: new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
...
```

## Roadmap

This is currently our roadmap, please feel free to request additions/changes.
Expand All @@ -168,7 +197,8 @@ This is currently our roadmap, please feel free to request additions/changes.
| Query variables ||
| Mutation variables ||
| Query polling ||
| Caching | 🔜 |
| In memory caching ||
| Offline caching ||
| Optimistic results | 🔜 |
| Client state management | 🔜 |

Expand All @@ -182,7 +212,7 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore -->
| [<img src="https://avatars2.githubusercontent.com/u/4757453?v=4" width="100px;"/><br /><sub><b>Eustatiu Dima</b></sub>](http://eusdima.com)<br />[💻](https://github.com/zino-app/graphql-flutter/commits?author=eusdima "Code") [📖](https://github.com/zino-app/graphql-flutter/commits?author=eusdima "Documentation") [💡](#example-eusdima "Examples") [🤔](#ideas-eusdima "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/17142193?v=4" width="100px;"/><br /><sub><b>Zino Hofmann</b></sub>](https://github.com/HofmannZ)<br />[💻](https://github.com/zino-app/graphql-flutter/commits?author=HofmannZ "Code") [📖](https://github.com/zino-app/graphql-flutter/commits?author=HofmannZ "Documentation") [💡](#example-HofmannZ "Examples") [🤔](#ideas-HofmannZ "Ideas, Planning, & Feedback") [👀](#review-HofmannZ "Reviewed Pull Requests") |
| [<img src="https://avatars2.githubusercontent.com/u/4757453?v=4" width="100px;"/><br /><sub><b>Eustatiu Dima</b></sub>](http://eusdima.com)<br />[💻](https://github.com/zino-app/graphql-flutter/commits?author=eusdima "Code") [📖](https://github.com/zino-app/graphql-flutter/commits?author=eusdima "Documentation") [💡](#example-eusdima "Examples") [🤔](#ideas-eusdima "Ideas, Planning, & Feedback") [👀](#review-eusdima "Reviewed Pull Requests") | [<img src="https://avatars3.githubusercontent.com/u/17142193?v=4" width="100px;"/><br /><sub><b>Zino Hofmann</b></sub>](https://github.com/HofmannZ)<br />[💻](https://github.com/zino-app/graphql-flutter/commits?author=HofmannZ "Code") [📖](https://github.com/zino-app/graphql-flutter/commits?author=HofmannZ "Documentation") [💡](#example-HofmannZ "Examples") [🤔](#ideas-HofmannZ "Ideas, Planning, & Feedback") [👀](#review-HofmannZ "Reviewed Pull Requests") |
| :---: | :---: |

<!-- ALL-CONTRIBUTORS-LIST:END -->
Expand Down
12 changes: 7 additions & 5 deletions example/app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ void main() {
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
return new CacheProvider(
child: new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion example/app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
graphql_flutter: ^0.3.0
graphql_flutter: ^0.4.0

dev_dependencies:
flutter_test:
Expand Down
8 changes: 6 additions & 2 deletions lib/graphql_flutter.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
library graphql_flutter;

export './src/client.dart';
export './src/query.dart';
export './src/mutation.dart';

export './src/cache/in_memory.dart';

export './src/widgets/query.dart';
export './src/widgets/mutation.dart';
export './src/widgets/cache_provider.dart';
91 changes: 91 additions & 0 deletions lib/src/cache/in_memory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';

import 'package:path_provider/path_provider.dart';

class InMemoryCache {
HashMap<String, dynamic> _inMemoryCache = new HashMap<String, dynamic>();

Future<String> get _localStoragePath async {
final Directory directory = await getApplicationDocumentsDirectory();

return directory.path;
}

Future<File> get _localStorageFile async {
final String path = await _localStoragePath;

return File('$path/cache.txt');
}

Future<dynamic> _writeToStorage() async {
final File file = await _localStorageFile;
IOSink sink = file.openWrite();

_inMemoryCache.forEach((key, value) {
sink.writeln(json.encode([key, value]));
});

sink.close();

return sink.done;
}

Future<HashMap<String, dynamic>> _readFromStorage() async {
try {
final File file = await _localStorageFile;
Stream<dynamic> inputStream = file.openRead();

final HashMap<String, dynamic> storedHashMap =
new HashMap<String, dynamic>();

inputStream
.transform(utf8.decoder) // Decode bytes to UTF8.
.transform(new LineSplitter()) // Convert stream to individual lines.
.listen((String line) {
final List keyAndValue = json.decode(line);

storedHashMap[keyAndValue[0]] = keyAndValue[1];
});

return storedHashMap;
} on FileSystemException {
// TODO: handle No such file

return new HashMap<String, dynamic>();
} catch (error) {
// TODO: handle error
print(error);

return new HashMap<String, dynamic>();
}
}

bool hasEntity(String key) => _inMemoryCache.containsKey(key);

void save() async {
await _writeToStorage();
}

void restore() async {
_inMemoryCache = await _readFromStorage();
}

dynamic read(String key) {
if (hasEntity(key)) {
return _inMemoryCache[key];
}

return null;
}

void write(String key, dynamic value) {
_inMemoryCache[key] = value;
}

void reset() {
_inMemoryCache.clear();
}
}
90 changes: 68 additions & 22 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@ import 'dart:convert';

import 'package:http/http.dart' as http;

import './cache/in_memory.dart';

Client client;

class Client {
Client({
String endPoint = '',
InMemoryCache cache,
}) {
this.endPoint = endPoint;
this.cache = cache;

this.client = new http.Client();
}

String _endpoint;
String _apiToken;
InMemoryCache _cache;

http.Client client;

Client([String endPoint = ""]) {
this.endPoint = endPoint;
this.client = new http.Client();
}

// Setters
set endPoint(String value) {
_endpoint = value;
Expand All @@ -25,6 +33,10 @@ class Client {
_apiToken = value;
}

set cache(InMemoryCache cache) {
_cache = cache;
}

// Getters
String get endPoint => this._endpoint;

Expand All @@ -35,27 +47,17 @@ class Client {
'Content-Type': 'application/json',
};

InMemoryCache get cache => this._cache;

// Methods
Future<Map<String, dynamic>> execute({
String query,
String _encodeBody(
String query, {
Map<String, dynamic> variables,
}) async {
final Map<String, dynamic> requestBody = {
}) {
return json.encode({
'query': query,
'variables': variables,
};

try {
final http.Response res = await client.post(
endPoint,
headers: headers,
body: json.encode(requestBody),
);

return _parseResponse(res);
} catch (error) {
throw error;
}
});
}

Map<String, dynamic> _parseResponse(http.Response response) {
Expand All @@ -78,4 +80,48 @@ class Client {

return jsonResponse['data'];
}

// The query method may send a request to your server if the appropriate data is not in your cache.
Future<Map<String, dynamic>> query({
String query,
Map<String, dynamic> variables,
}) async {
final String body = _encodeBody(
query,
variables: variables,
);

try {
final http.Response res = await client.post(
endPoint,
headers: headers,
body: body,
);

final Map<String, dynamic> parsedResponse = _parseResponse(res);

cache.write(body, parsedResponse);

return parsedResponse;
} catch (error) {
throw error;
}
}

// The readQuery method is very similar to the query method except that readQuery will never make a request to your GraphQL server.
Map<String, dynamic> readQuery({
String query,
Map<String, dynamic> variables,
}) {
final String body = _encodeBody(
query,
variables: variables,
);

if (cache.hasEntity(body)) {
return cache.read(body);
} else {
throw new Exception('Can\'t find field in cache.');
}
}
}
Loading

0 comments on commit c9687ca

Please sign in to comment.