Skip to content

Commit

Permalink
ok_http: Add BaseClient Implementation and make asynchronous reques…
Browse files Browse the repository at this point in the history
…ts. (#1215)
  • Loading branch information
Anikate-De committed May 31, 2024
1 parent 6337ee3 commit 7bfbeea
Show file tree
Hide file tree
Showing 24 changed files with 8,853 additions and 140 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/okhttp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,14 @@ jobs:
- name: Analyze code
if: always() && steps.install.outcome == 'success'
run: flutter analyze --fatal-infos
- name: Run tests
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
if: always() && steps.install.outcome == 'success'
with:
# api-level/minSdkVersion should be help in sync in:
# - .github/workflows/ok.yml
# - pkgs/ok_http/android/build.gradle
# - pkgs/ok_http/example/android/app/build.gradle
api-level: 21
arch: x86_64
script: cd pkgs/ok_http/example && flutter test --timeout=120s integration_test/
3 changes: 3 additions & 0 deletions pkgs/ok_http/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ migrate_working_dir/
**/doc/api/
.dart_tool/
build/

# Ignore the JAR files required to generate JNI Bindings
jar/
3 changes: 2 additions & 1 deletion pkgs/ok_http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## 0.1.0-wip

* Initial release.
- 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)
- `ok_http` can now send asynchronous requests
4 changes: 4 additions & 0 deletions pkgs/ok_http/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
include: ../../analysis_options.yaml

analyzer:
exclude:
- lib/src/third_party/
19 changes: 4 additions & 15 deletions pkgs/ok_http/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,6 @@ android {
// (e.g. ndkVersion "23.1.7779620")
ndkVersion = android.ndkVersion

// Invoke the shared CMake build with the Android Gradle Plugin.
externalNativeBuild {
cmake {
path = "../src/CMakeLists.txt"

// The default CMake version for the Android Gradle Plugin is 3.10.2.
// https://developer.android.com/studio/projects/install-ndk#vanilla_cmake
//
// The Flutter tooling requires that developers have CMake 3.10 or later
// installed. You should not increase this version, as doing so will cause
// the plugin to fail to compile for some customers of the plugin.
// version "3.10.2"
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
Expand All @@ -63,3 +48,7 @@ android {
minSdk = 21
}
}

dependencies {
implementation('com.squareup.okhttp3:okhttp:4.12.0')
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<application
android:label="ok_http_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
2 changes: 1 addition & 1 deletion pkgs/ok_http/example/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
}

include ":app"
27 changes: 27 additions & 0 deletions pkgs/ok_http/example/integration_test/client_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:http_client_conformance_tests/http_client_conformance_tests.dart';
import 'package:integration_test/integration_test.dart';
import 'package:ok_http/ok_http.dart';
import 'package:test/test.dart';

void main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

await testConformance();
}

Future<void> testConformance() async {
group('ok_http client', () {
testRequestBody(OkHttpClient());
testResponseBody(OkHttpClient(), canStreamResponseBody: false);
testRequestHeaders(OkHttpClient());
testRequestMethods(OkHttpClient(), preservesMethodCase: true);
testResponseStatusLine(OkHttpClient());
testCompressedResponseBody(OkHttpClient());
testIsolate(OkHttpClient.new);
testResponseCookies(OkHttpClient(), canReceiveSetCookieHeaders: true);
});
}
32 changes: 32 additions & 0 deletions pkgs/ok_http/example/lib/book.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

class Book {
String title;
String description;
Uri imageUrl;

Book(this.title, this.description, this.imageUrl);

static List<Book> listFromJson(Map<dynamic, dynamic> json) {
final books = <Book>[];

if (json['items'] case final List<dynamic> items) {
for (final item in items) {
if (item case {'volumeInfo': final Map<dynamic, dynamic> volumeInfo}) {
if (volumeInfo
case {
'title': final String title,
'description': final String description,
'imageLinks': {'smallThumbnail': final String thumbnail}
}) {
books.add(Book(title, description, Uri.parse(thumbnail)));
}
}
}
}

return books;
}
}
172 changes: 126 additions & 46 deletions pkgs/ok_http/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,69 +1,149 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:http_image_provider/http_image_provider.dart';
import 'package:ok_http/ok_http.dart';
import 'package:provider/provider.dart';

import 'book.dart';

void main() {
runApp(const MyApp());
final Client httpClient;
if (Platform.isAndroid) {
httpClient = OkHttpClient();
} else {
httpClient = IOClient(HttpClient()..userAgent = 'Book Agent');
}

runApp(Provider<Client>(
create: (_) => httpClient,
child: const BookSearchApp(),
dispose: (_, client) => client.close()));
}

class MyApp extends StatefulWidget {
const MyApp({super.key});
class BookSearchApp extends StatelessWidget {
const BookSearchApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
Widget build(BuildContext context) => const MaterialApp(
// Remove the debug banner.
debugShowCheckedModeBanner: false,
title: 'Book Search',
home: HomePage(),
);
}

class _MyAppState extends State<MyApp> {
late int sumResult;
late Future<int> sumAsyncResult;
class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
List<Book>? _books;
String? _lastQuery;
late Client _client;

@override
void initState() {
super.initState();
_client = context.read<Client>();
}

// Get the list of books matching `query`.
// The `get` call will automatically use the `client` configured in `main`.
Future<List<Book>> _findMatchingBooks(String query) async {
final response = await _client.get(
Uri.https(
'www.googleapis.com',
'/books/v1/volumes',
{'q': query, 'maxResults': '20', 'printType': 'books'},
),
);

final json = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
return Book.listFromJson(json);
}

void _runSearch(String query) async {
_lastQuery = query;
if (query.isEmpty) {
setState(() {
_books = null;
});
return;
}

final books = await _findMatchingBooks(query);
// Avoid the situation where a slow-running query finishes late and
// replaces newer search results.
if (query != _lastQuery) return;
setState(() {
_books = books;
});
}

@override
Widget build(BuildContext context) {
const textStyle = TextStyle(fontSize: 25);
const spacerSmall = SizedBox(height: 10);
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Native Packages'),
),
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
const Text(
'',
style: textStyle,
textAlign: TextAlign.center,
),
spacerSmall,
Text(
'sum(1, 2) = $sumResult',
style: textStyle,
textAlign: TextAlign.center,
),
spacerSmall,
FutureBuilder<int>(
future: sumAsyncResult,
builder: (BuildContext context, AsyncSnapshot<int> value) {
final displayValue =
(value.hasData) ? value.data : 'loading';
return Text(
'await sumAsync(3, 4) = $displayValue',
style: textStyle,
textAlign: TextAlign.center,
);
},
),
],
final searchResult = _books == null
? const Text('Please enter a query', style: TextStyle(fontSize: 24))
: _books!.isNotEmpty
? BookList(_books!)
: const Text('No results found', style: TextStyle(fontSize: 24));

return Scaffold(
appBar: AppBar(title: const Text('Book Search')),
body: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
const SizedBox(height: 20),
TextField(
onChanged: _runSearch,
decoration: const InputDecoration(
labelText: 'Search',
suffixIcon: Icon(Icons.search),
),
),
),
const SizedBox(height: 20),
Expanded(child: searchResult),
],
),
),
);
}
}

class BookList extends StatefulWidget {
final List<Book> books;
const BookList(this.books, {super.key});

@override
State<BookList> createState() => _BookListState();
}

class _BookListState extends State<BookList> {
@override
Widget build(BuildContext context) => ListView.builder(
itemCount: widget.books.length,
itemBuilder: (context, index) => Card(
key: ValueKey(widget.books[index].title),
child: ListTile(
leading: Image(
image: HttpImage(
widget.books[index].imageUrl.replace(scheme: 'https'),
client: context.read<Client>())),
title: Text(widget.books[index].title),
subtitle: Text(widget.books[index].description),
),
),
);
}
Loading

0 comments on commit 7bfbeea

Please sign in to comment.