Skip to content

Commit d434d42

Browse files
authored
Make it possible to use a custom CronetEngine with runWithClient (#843)
1 parent 38d5dd9 commit d434d42

File tree

7 files changed

+146
-40
lines changed

7 files changed

+146
-40
lines changed

pkgs/cronet_http/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.1.0
2+
3+
* Add a CronetClient that accepts a `Future<CronetEngine>`.
4+
* Modify the example application to create a `CronetClient` using a
5+
`Future<CronetEngine>`.
6+
17
## 0.0.4
28

39
* Fix a bug where the example would not use the configured `package:http`

pkgs/cronet_http/example/integration_test/client_conformance_test.dart

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:cronet_http/cronet_client.dart';
6+
import 'package:http/http.dart';
7+
import 'package:http_client_conformance_tests/http_client_conformance_tests.dart';
8+
import 'package:integration_test/integration_test.dart';
9+
import 'package:test/test.dart';
10+
11+
void testClientConformance(CronetClient Function() clientFactory) {
12+
// TODO: Use `testAll` when `testServerErrors` passes i.e.
13+
// testAll(CronetClient(), canStreamRequestBody: false);
14+
15+
final client = clientFactory();
16+
testRequestBody(client);
17+
testRequestBodyStreamed(client, canStreamRequestBody: false);
18+
testResponseBody(client);
19+
testResponseBodyStreamed(client);
20+
testRequestHeaders(client);
21+
testResponseHeaders(client);
22+
testRedirect(client);
23+
testCompressedResponseBody(client);
24+
testMultipleClients(clientFactory);
25+
}
26+
27+
Future<void> testConformance() async {
28+
group('default cronet engine',
29+
() => testClientConformance(CronetClient.defaultCronetEngine));
30+
31+
final engine = await CronetEngine.build(
32+
cacheMode: CacheMode.disabled, userAgent: 'Test Agent (Engine)');
33+
34+
group('from cronet engine', () {
35+
testClientConformance(() => CronetClient.fromCronetEngine(engine));
36+
});
37+
38+
group('from cronet engine future', () {
39+
final engineFuture = CronetEngine.build(
40+
cacheMode: CacheMode.disabled, userAgent: 'Test Agent (Future)');
41+
testClientConformance(
42+
() => CronetClient.fromCronetEngineFuture(engineFuture));
43+
});
44+
}
45+
46+
Future<void> testClientFromFutureFails() async {
47+
test('cronet engine future fails', () async {
48+
final engineFuture = CronetEngine.build(
49+
cacheMode: CacheMode.disk,
50+
storagePath: '/non-existant-path/', // Will cause `build` to throw.
51+
userAgent: 'Test Agent (Future)');
52+
53+
final client = CronetClient.fromCronetEngineFuture(engineFuture);
54+
await expectLater(
55+
client.get(Uri.http('example.com', '/')),
56+
throwsA((Exception e) =>
57+
e is ClientException &&
58+
e.message.contains('Exception building CronetEngine: '
59+
'Invalid argument(s): Storage path must')));
60+
});
61+
}
62+
63+
void main() async {
64+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
65+
66+
await testConformance();
67+
await testClientFromFutureFails();
68+
}

pkgs/cronet_http/example/integration_test/cronet_configuration_test.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ void testCache() {
3434

3535
test('disabled', () async {
3636
final engine = await CronetEngine.build(cacheMode: CacheMode.disabled);
37-
final client = CronetClient(engine);
37+
final client = CronetClient.fromCronetEngine(engine);
3838
await client.get(Uri.parse('http://localhost:${server.port}'));
3939
await client.get(Uri.parse('http://localhost:${server.port}'));
4040
expect(numRequests, 2);
@@ -43,7 +43,7 @@ void testCache() {
4343
test('memory', () async {
4444
final engine = await CronetEngine.build(
4545
cacheMode: CacheMode.memory, cacheMaxSize: 1024 * 1024);
46-
final client = CronetClient(engine);
46+
final client = CronetClient.fromCronetEngine(engine);
4747
await client.get(Uri.parse('http://localhost:${server.port}'));
4848
await client.get(Uri.parse('http://localhost:${server.port}'));
4949
expect(numRequests, 1);
@@ -54,7 +54,7 @@ void testCache() {
5454
cacheMode: CacheMode.disk,
5555
cacheMaxSize: 1024 * 1024,
5656
storagePath: (await Directory.systemTemp.createTemp()).absolute.path);
57-
final client = CronetClient(engine);
57+
final client = CronetClient.fromCronetEngine(engine);
5858
await client.get(Uri.parse('http://localhost:${server.port}'));
5959
await client.get(Uri.parse('http://localhost:${server.port}'));
6060
expect(numRequests, 1);
@@ -66,7 +66,7 @@ void testCache() {
6666
cacheMaxSize: 1024 * 1024,
6767
storagePath: (await Directory.systemTemp.createTemp()).absolute.path);
6868

69-
final client = CronetClient(engine);
69+
final client = CronetClient.fromCronetEngine(engine);
7070
await client.get(Uri.parse('http://localhost:${server.port}'));
7171
await client.get(Uri.parse('http://localhost:${server.port}'));
7272
expect(numRequests, 2);
@@ -115,7 +115,7 @@ void testUserAgent() {
115115

116116
test('userAgent', () async {
117117
final engine = await CronetEngine.build(userAgent: 'fake-agent');
118-
await CronetClient(engine)
118+
await CronetClient.fromCronetEngine(engine)
119119
.get(Uri.parse('http://localhost:${server.port}'));
120120
expect(requestHeaders['user-agent'], ['fake-agent']);
121121
});

pkgs/cronet_http/example/lib/main.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import 'book.dart';
1515
void main() {
1616
var clientFactory = Client.new; // Constructs the default client.
1717
if (Platform.isAndroid) {
18-
clientFactory = CronetClient.new;
18+
Future<CronetEngine>? engine;
19+
clientFactory = () {
20+
engine ??= CronetEngine.build(
21+
cacheMode: CacheMode.memory, userAgent: 'Book Agent');
22+
return CronetClient.fromCronetEngineFuture(engine!);
23+
};
1924
}
2025
runWithClient(() => runApp(const BookSearchApp()), clientFactory);
2126
}

pkgs/cronet_http/lib/cronet_client.dart

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
/// An Android Flutter plugin that provides access to the
6+
/// [Cronet](https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/package-summary)
7+
/// HTTP client.
8+
///
9+
/// The platform interface must be initialized before using this plugin e.g. by
10+
/// calling
11+
/// [`WidgetsFlutterBinding.ensureInitialized`](https://api.flutter.dev/flutter/widgets/WidgetsFlutterBinding/ensureInitialized.html)
12+
/// or
13+
/// [`runApp`](https://api.flutter.dev/flutter/widgets/runApp.html).
14+
515
import 'dart:async';
616

717
import 'package:flutter/services.dart';
@@ -123,11 +133,40 @@ class CronetEngine {
123133
/// ```
124134
class CronetClient extends BaseClient {
125135
CronetEngine? _engine;
136+
Future<CronetEngine>? _engineFuture;
137+
138+
/// Indicates that [_engine] was constructed as an implementation detail for
139+
/// this [CronetClient] (i.e. was not provided as a constructor argument) and
140+
/// should be closed when this [CronetClient] is closed.
126141
final bool _ownedEngine;
127142

128-
CronetClient([CronetEngine? engine])
129-
: _engine = engine,
130-
_ownedEngine = engine == null;
143+
CronetClient._(this._engineFuture, this._ownedEngine);
144+
145+
/// A [CronetClient] that will be initialized with a new [CronetEngine].
146+
factory CronetClient.defaultCronetEngine() => CronetClient._(null, true);
147+
148+
/// A [CronetClient] configured with a [CronetEngine].
149+
factory CronetClient.fromCronetEngine(CronetEngine engine) =>
150+
CronetClient._(Future.value(engine), false);
151+
152+
/// A [CronetClient] configured with a [Future] containing a [CronetEngine].
153+
///
154+
/// This can be useful in circumstances where a non-Future [CronetClient] is
155+
/// required but you want to configure the [CronetClient] with a custom
156+
/// [CronetEngine]. For example:
157+
/// ```
158+
/// void main() {
159+
/// Client clientFactory() {
160+
/// final engine = CronetEngine.build(
161+
/// cacheMode: CacheMode.memory, userAgent: 'Book Agent');
162+
/// return CronetClient.fromCronetEngineFuture(engine);
163+
/// }
164+
///
165+
/// runWithClient(() => runApp(const BookSearchApp()), clientFactory);
166+
/// }
167+
/// ```
168+
factory CronetClient.fromCronetEngineFuture(Future<CronetEngine> engine) =>
169+
CronetClient._(engine, false);
131170

132171
@override
133172
void close() {
@@ -138,11 +177,23 @@ class CronetClient extends BaseClient {
138177

139178
@override
140179
Future<StreamedResponse> send(BaseRequest request) async {
141-
try {
142-
_engine ??= await CronetEngine.build();
143-
} catch (e) {
144-
throw ClientException(e.toString(), request.url);
180+
if (_engine == null) {
181+
// Create the future here rather than in the [fromCronetEngineFuture]
182+
// factory so that [close] does not have to await the future just to
183+
// close it in the case where [send] is never called.
184+
//
185+
// Assign to _engineFuture instead of just `await`ing the result of
186+
// `CronetEngine.build()` to prevent concurrent executions of `send`
187+
// from creating multiple [CronetEngine]s.
188+
_engineFuture ??= CronetEngine.build();
189+
try {
190+
_engine = await _engineFuture;
191+
} catch (e) {
192+
throw ClientException(
193+
'Exception building CronetEngine: ${e.toString()}', request.url);
194+
}
145195
}
196+
146197
final stream = request.finalize();
147198

148199
final body = await stream.toBytes();
@@ -203,7 +254,8 @@ class CronetClient extends BaseClient {
203254
});
204255

205256
final result = await responseCompleter.future;
206-
final responseHeaders = (result.headers.cast<String, List<Object?>>())
257+
final responseHeaders = result.headers
258+
.cast<String, List<Object?>>()
207259
.map((key, value) => MapEntry(key.toLowerCase(), value.join(',')));
208260

209261
final contentLengthHeader = responseHeaders['content-length'];

pkgs/cronet_http/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: cronet_http
22
description: >
33
An Android Flutter plugin that provides access to the Cronet HTTP client.
4-
version: 0.0.3
4+
version: 0.1.0
55
repository: https://github.com/dart-lang/http/tree/master/pkgs/cronet_http
66

77
environment:

0 commit comments

Comments
 (0)