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

ok_http: Add BaseClient Implementation and make asynchronous requests. #1215

Merged
merged 15 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
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
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', () {
Anikate-De marked this conversation as resolved.
Show resolved Hide resolved
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
Loading