Skip to content

Commit 7bfbeea

Browse files
authored
ok_http: Add BaseClient Implementation and make asynchronous requests. (#1215)
1 parent 6337ee3 commit 7bfbeea

24 files changed

+8853
-140
lines changed

.github/workflows/okhttp.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,14 @@ jobs:
4545
- name: Analyze code
4646
if: always() && steps.install.outcome == 'success'
4747
run: flutter analyze --fatal-infos
48+
- name: Run tests
49+
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
50+
if: always() && steps.install.outcome == 'success'
51+
with:
52+
# api-level/minSdkVersion should be help in sync in:
53+
# - .github/workflows/ok.yml
54+
# - pkgs/ok_http/android/build.gradle
55+
# - pkgs/ok_http/example/android/app/build.gradle
56+
api-level: 21
57+
arch: x86_64
58+
script: cd pkgs/ok_http/example && flutter test --timeout=120s integration_test/

pkgs/ok_http/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ migrate_working_dir/
2727
**/doc/api/
2828
.dart_tool/
2929
build/
30+
31+
# Ignore the JAR files required to generate JNI Bindings
32+
jar/

pkgs/ok_http/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
## 0.1.0-wip
22

3-
* Initial release.
3+
- Implementation of [`BaseClient`](https://pub.dev/documentation/http/latest/http/BaseClient-class.html) and `send()` method using [`enqueue()` API](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-call/enqueue.html)
4+
- `ok_http` can now send asynchronous requests

pkgs/ok_http/analysis_options.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
include: ../../analysis_options.yaml
2+
3+
analyzer:
4+
exclude:
5+
- lib/src/third_party/

pkgs/ok_http/android/build.gradle

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,6 @@ android {
3939
// (e.g. ndkVersion "23.1.7779620")
4040
ndkVersion = android.ndkVersion
4141

42-
// Invoke the shared CMake build with the Android Gradle Plugin.
43-
externalNativeBuild {
44-
cmake {
45-
path = "../src/CMakeLists.txt"
46-
47-
// The default CMake version for the Android Gradle Plugin is 3.10.2.
48-
// https://developer.android.com/studio/projects/install-ndk#vanilla_cmake
49-
//
50-
// The Flutter tooling requires that developers have CMake 3.10 or later
51-
// installed. You should not increase this version, as doing so will cause
52-
// the plugin to fail to compile for some customers of the plugin.
53-
// version "3.10.2"
54-
}
55-
}
56-
5742
compileOptions {
5843
sourceCompatibility = JavaVersion.VERSION_1_8
5944
targetCompatibility = JavaVersion.VERSION_1_8
@@ -63,3 +48,7 @@ android {
6348
minSdk = 21
6449
}
6550
}
51+
52+
dependencies {
53+
implementation('com.squareup.okhttp3:okhttp:4.12.0')
54+
}

pkgs/ok_http/example/android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<application
33
android:label="ok_http_example"
44
android:name="${applicationName}"
5-
android:icon="@mipmap/ic_launcher">
5+
android:icon="@mipmap/ic_launcher"
6+
android:usesCleartextTraffic="true">
67
<activity
78
android:name=".MainActivity"
89
android:exported="true"

pkgs/ok_http/example/android/settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pluginManagement {
1919
plugins {
2020
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
2121
id "com.android.application" version "7.3.0" apply false
22-
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
22+
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
2323
}
2424

2525
include ":app"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2024, 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:http_client_conformance_tests/http_client_conformance_tests.dart';
6+
import 'package:integration_test/integration_test.dart';
7+
import 'package:ok_http/ok_http.dart';
8+
import 'package:test/test.dart';
9+
10+
void main() async {
11+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
12+
13+
await testConformance();
14+
}
15+
16+
Future<void> testConformance() async {
17+
group('ok_http client', () {
18+
testRequestBody(OkHttpClient());
19+
testResponseBody(OkHttpClient(), canStreamResponseBody: false);
20+
testRequestHeaders(OkHttpClient());
21+
testRequestMethods(OkHttpClient(), preservesMethodCase: true);
22+
testResponseStatusLine(OkHttpClient());
23+
testCompressedResponseBody(OkHttpClient());
24+
testIsolate(OkHttpClient.new);
25+
testResponseCookies(OkHttpClient(), canReceiveSetCookieHeaders: true);
26+
});
27+
}

pkgs/ok_http/example/lib/book.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2024, 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+
class Book {
6+
String title;
7+
String description;
8+
Uri imageUrl;
9+
10+
Book(this.title, this.description, this.imageUrl);
11+
12+
static List<Book> listFromJson(Map<dynamic, dynamic> json) {
13+
final books = <Book>[];
14+
15+
if (json['items'] case final List<dynamic> items) {
16+
for (final item in items) {
17+
if (item case {'volumeInfo': final Map<dynamic, dynamic> volumeInfo}) {
18+
if (volumeInfo
19+
case {
20+
'title': final String title,
21+
'description': final String description,
22+
'imageLinks': {'smallThumbnail': final String thumbnail}
23+
}) {
24+
books.add(Book(title, description, Uri.parse(thumbnail)));
25+
}
26+
}
27+
}
28+
}
29+
30+
return books;
31+
}
32+
}

pkgs/ok_http/example/lib/main.dart

Lines changed: 126 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,149 @@
1+
// Copyright (c) 2024, 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 'dart:convert';
6+
import 'dart:io';
7+
18
import 'package:flutter/material.dart';
2-
import 'dart:async';
9+
import 'package:http/http.dart';
10+
import 'package:http/io_client.dart';
11+
import 'package:http_image_provider/http_image_provider.dart';
12+
import 'package:ok_http/ok_http.dart';
13+
import 'package:provider/provider.dart';
14+
15+
import 'book.dart';
316

417
void main() {
5-
runApp(const MyApp());
18+
final Client httpClient;
19+
if (Platform.isAndroid) {
20+
httpClient = OkHttpClient();
21+
} else {
22+
httpClient = IOClient(HttpClient()..userAgent = 'Book Agent');
23+
}
24+
25+
runApp(Provider<Client>(
26+
create: (_) => httpClient,
27+
child: const BookSearchApp(),
28+
dispose: (_, client) => client.close()));
629
}
730

8-
class MyApp extends StatefulWidget {
9-
const MyApp({super.key});
31+
class BookSearchApp extends StatelessWidget {
32+
const BookSearchApp({super.key});
1033

1134
@override
12-
State<MyApp> createState() => _MyAppState();
35+
Widget build(BuildContext context) => const MaterialApp(
36+
// Remove the debug banner.
37+
debugShowCheckedModeBanner: false,
38+
title: 'Book Search',
39+
home: HomePage(),
40+
);
1341
}
1442

15-
class _MyAppState extends State<MyApp> {
16-
late int sumResult;
17-
late Future<int> sumAsyncResult;
43+
class HomePage extends StatefulWidget {
44+
const HomePage({super.key});
45+
46+
@override
47+
State<HomePage> createState() => _HomePageState();
48+
}
49+
50+
class _HomePageState extends State<HomePage> {
51+
List<Book>? _books;
52+
String? _lastQuery;
53+
late Client _client;
1854

1955
@override
2056
void initState() {
2157
super.initState();
58+
_client = context.read<Client>();
59+
}
60+
61+
// Get the list of books matching `query`.
62+
// The `get` call will automatically use the `client` configured in `main`.
63+
Future<List<Book>> _findMatchingBooks(String query) async {
64+
final response = await _client.get(
65+
Uri.https(
66+
'www.googleapis.com',
67+
'/books/v1/volumes',
68+
{'q': query, 'maxResults': '20', 'printType': 'books'},
69+
),
70+
);
71+
72+
final json = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
73+
return Book.listFromJson(json);
74+
}
75+
76+
void _runSearch(String query) async {
77+
_lastQuery = query;
78+
if (query.isEmpty) {
79+
setState(() {
80+
_books = null;
81+
});
82+
return;
83+
}
84+
85+
final books = await _findMatchingBooks(query);
86+
// Avoid the situation where a slow-running query finishes late and
87+
// replaces newer search results.
88+
if (query != _lastQuery) return;
89+
setState(() {
90+
_books = books;
91+
});
2292
}
2393

2494
@override
2595
Widget build(BuildContext context) {
26-
const textStyle = TextStyle(fontSize: 25);
27-
const spacerSmall = SizedBox(height: 10);
28-
return MaterialApp(
29-
home: Scaffold(
30-
appBar: AppBar(
31-
title: const Text('Native Packages'),
32-
),
33-
body: SingleChildScrollView(
34-
child: Container(
35-
padding: const EdgeInsets.all(10),
36-
child: Column(
37-
children: [
38-
const Text(
39-
'',
40-
style: textStyle,
41-
textAlign: TextAlign.center,
42-
),
43-
spacerSmall,
44-
Text(
45-
'sum(1, 2) = $sumResult',
46-
style: textStyle,
47-
textAlign: TextAlign.center,
48-
),
49-
spacerSmall,
50-
FutureBuilder<int>(
51-
future: sumAsyncResult,
52-
builder: (BuildContext context, AsyncSnapshot<int> value) {
53-
final displayValue =
54-
(value.hasData) ? value.data : 'loading';
55-
return Text(
56-
'await sumAsync(3, 4) = $displayValue',
57-
style: textStyle,
58-
textAlign: TextAlign.center,
59-
);
60-
},
61-
),
62-
],
96+
final searchResult = _books == null
97+
? const Text('Please enter a query', style: TextStyle(fontSize: 24))
98+
: _books!.isNotEmpty
99+
? BookList(_books!)
100+
: const Text('No results found', style: TextStyle(fontSize: 24));
101+
102+
return Scaffold(
103+
appBar: AppBar(title: const Text('Book Search')),
104+
body: Padding(
105+
padding: const EdgeInsets.all(10),
106+
child: Column(
107+
children: [
108+
const SizedBox(height: 20),
109+
TextField(
110+
onChanged: _runSearch,
111+
decoration: const InputDecoration(
112+
labelText: 'Search',
113+
suffixIcon: Icon(Icons.search),
114+
),
63115
),
64-
),
116+
const SizedBox(height: 20),
117+
Expanded(child: searchResult),
118+
],
65119
),
66120
),
67121
);
68122
}
69123
}
124+
125+
class BookList extends StatefulWidget {
126+
final List<Book> books;
127+
const BookList(this.books, {super.key});
128+
129+
@override
130+
State<BookList> createState() => _BookListState();
131+
}
132+
133+
class _BookListState extends State<BookList> {
134+
@override
135+
Widget build(BuildContext context) => ListView.builder(
136+
itemCount: widget.books.length,
137+
itemBuilder: (context, index) => Card(
138+
key: ValueKey(widget.books[index].title),
139+
child: ListTile(
140+
leading: Image(
141+
image: HttpImage(
142+
widget.books[index].imageUrl.replace(scheme: 'https'),
143+
client: context.read<Client>())),
144+
title: Text(widget.books[index].title),
145+
subtitle: Text(widget.books[index].description),
146+
),
147+
),
148+
);
149+
}

0 commit comments

Comments
 (0)