diff --git a/pkgs/cronet_http/CHANGELOG.md b/pkgs/cronet_http/CHANGELOG.md index 24171067fd..55593ac913 100644 --- a/pkgs/cronet_http/CHANGELOG.md +++ b/pkgs/cronet_http/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.9.1-wip + +* Update example to fetch and display scores of package:cronet_http from pub.dev. + ## 1.9.0 * Add `CronetEngine.startNetLogToFile` and `CronetEngine.stopNetLog`. diff --git a/pkgs/cronet_http/README.md b/pkgs/cronet_http/README.md index eca84103b0..b31da2c6fc 100644 --- a/pkgs/cronet_http/README.md +++ b/pkgs/cronet_http/README.md @@ -40,18 +40,14 @@ void main() async { final engine = CronetEngine.build( cacheMode: CacheMode.memory, cacheMaxSize: 2 * 1024 * 1024, - userAgent: 'Book Agent'); + userAgent: 'Package Client'); httpClient = CronetClient.fromCronetEngine(engine, closeEngine: true); } else { - httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); + httpClient = IOClient(HttpClient()..userAgent = 'Package Client'); } - final response = await client.get( - Uri.https( - 'www.googleapis.com', - '/books/v1/volumes', - {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'}, - ), + final response = await httpClient.get( + Uri.https('pub.dev', '/api/packages/cronet_http/score'), ); httpClient.close(); } diff --git a/pkgs/cronet_http/example/android/gradle.properties b/pkgs/cronet_http/example/android/gradle.properties index 988bcc6f83..cc738c5387 100644 --- a/pkgs/cronet_http/example/android/gradle.properties +++ b/pkgs/cronet_http/example/android/gradle.properties @@ -2,3 +2,7 @@ org.gradle.jvmargs=-Xmx12800M org.gradle.caching=true android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/pkgs/cronet_http/example/lib/book.dart b/pkgs/cronet_http/example/lib/book.dart deleted file mode 100644 index 584ae9a361..0000000000 --- a/pkgs/cronet_http/example/lib/book.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2023, 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 listFromJson(Map json) { - final books = []; - - if (json['items'] case final List items) { - for (final item in items) { - if (item case {'volumeInfo': final Map 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; - } -} diff --git a/pkgs/cronet_http/example/lib/main.dart b/pkgs/cronet_http/example/lib/main.dart index 37136a2afb..d9d171581c 100644 --- a/pkgs/cronet_http/example/lib/main.dart +++ b/pkgs/cronet_http/example/lib/main.dart @@ -9,11 +9,8 @@ import 'package:cronet_http/cronet_http.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; -import 'package:http_image_provider/http_image_provider.dart'; import 'package:provider/provider.dart'; -import 'book.dart'; - void main() { final Client httpClient; if (Platform.isAndroid) { @@ -21,133 +18,80 @@ void main() { final engine = CronetEngine.build( cacheMode: CacheMode.memory, cacheMaxSize: 2 * 1024 * 1024, - userAgent: 'Book Agent'); + userAgent: 'Package Client'); httpClient = CronetClient.fromCronetEngine(engine, closeEngine: true); } else { - httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); + httpClient = IOClient(HttpClient()..userAgent = 'Package Client'); } runApp(Provider( create: (_) => httpClient, - child: const BookSearchApp(), + child: const PackageDetailsApp(), dispose: (_, client) => client.close())); } -class BookSearchApp extends StatelessWidget { - const BookSearchApp({super.key}); +class PackageDetailsApp extends StatelessWidget { + const PackageDetailsApp({super.key}); @override Widget build(BuildContext context) => const MaterialApp( - // Remove the debug banner. debugShowCheckedModeBanner: false, - title: 'Book Search', - home: HomePage(), + title: 'Package Details', + home: PackageDetailsPage(), ); } -class HomePage extends StatefulWidget { - const HomePage({super.key}); +class PackageDetailsPage extends StatefulWidget { + const PackageDetailsPage({super.key}); @override - State createState() => _HomePageState(); + State createState() => _PackageDetailsPageState(); } -class _HomePageState extends State { - List? _books; - String? _lastQuery; - late Client _client; +class _PackageDetailsPageState extends State { + String _output = 'Loading...'; @override void initState() { super.initState(); - _client = context.read(); - } - - // Get the list of books matching `query`. - // The `get` call will automatically use the `client` configured in `main`. - Future> _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); + _fetchPackageInfo(); } - void _runSearch(String query) async { - _lastQuery = query; - if (query.isEmpty) { + void _fetchPackageInfo() async { + final client = context.read(); + try { + final response = await client.get( + Uri.https('pub.dev', '/api/packages/cronet_http/score'), + ); + if (response.statusCode == 200) { + final json = + jsonDecode(utf8.decode(response.bodyBytes)) as Map; + setState(() { + _output = 'Information about package:cronet_http:\n' + '- Likes: ${json['likeCount']}\n' + '- 30-day downloads: ${json['downloadCount30Days']}'; + }); + } else { + setState(() { + _output = 'Request failed with status: ${response.statusCode}.'; + }); + } + } catch (e) { setState(() { - _books = null; + _output = 'Request failed: $e'; }); - 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) { - 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), - ), + Widget build(BuildContext context) => Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + _output, + style: const TextStyle(fontSize: 18, fontFamily: 'monospace'), ), - const SizedBox(height: 20), - Expanded(child: searchResult), - ], - ), - ), - ); - } -} - -class BookList extends StatefulWidget { - final List books; - const BookList(this.books, {super.key}); - - @override - State createState() => _BookListState(); -} - -class _BookListState extends State { - @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: HttpImageProvider( - widget.books[index].imageUrl.replace(scheme: 'https'), - client: context.read())), - title: Text(widget.books[index].title), - subtitle: Text(widget.books[index].description), ), ), ); diff --git a/pkgs/cronet_http/example/pubspec.yaml b/pkgs/cronet_http/example/pubspec.yaml index 79fc4fa89c..d27d53bb76 100644 --- a/pkgs/cronet_http/example/pubspec.yaml +++ b/pkgs/cronet_http/example/pubspec.yaml @@ -9,11 +9,9 @@ environment: dependencies: cronet_http: path: ../ - cupertino_icons: ^1.0.2 flutter: sdk: flutter http: ^1.0.0 - http_image_provider: ^1.0.0 provider: ^6.1.1 dev_dependencies: diff --git a/pkgs/cronet_http/lib/cronet_http.dart b/pkgs/cronet_http/lib/cronet_http.dart index 53c9aca019..4a7fe5381f 100644 --- a/pkgs/cronet_http/lib/cronet_http.dart +++ b/pkgs/cronet_http/lib/cronet_http.dart @@ -28,15 +28,16 @@ /// final engine = CronetEngine.build( /// cacheMode: CacheMode.memory, /// cacheMaxSize: 2 * 1024 * 1024, -/// userAgent: 'Book Agent'); +/// userAgent: 'Package Client'); /// httpClient = CronetClient.fromCronetEngine(engine, closeEngine: true); /// } else { -/// httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); +/// httpClient = IOClient( +/// HttpClient()..userAgent = 'Package Client'); /// } /// /// runApp(Provider( /// create: (_) => httpClient, -/// child: const BookSearchApp(), +/// child: const PackageDetailsApp(), /// dispose: (_, client) => client.close())); /// } /// } diff --git a/pkgs/cronet_http/lib/src/cronet_client.dart b/pkgs/cronet_http/lib/src/cronet_client.dart index 6ccce6eb59..70d11254e3 100644 --- a/pkgs/cronet_http/lib/src/cronet_client.dart +++ b/pkgs/cronet_http/lib/src/cronet_client.dart @@ -629,11 +629,11 @@ class CronetClient extends BaseClient { /// void main() { /// Client clientFactory() { /// final engine = CronetEngine.build( - /// cacheMode: CacheMode.memory, userAgent: 'Book Agent'); + /// cacheMode: CacheMode.memory, userAgent: 'Package Client'); /// return CronetClient.fromCronetEngineFuture(engine); /// } /// - /// runWithClient(() => runApp(const BookSearchApp()), clientFactory); + /// runWithClient(() => runApp(const PackageDetailsApp()), clientFactory); /// } /// ``` @override diff --git a/pkgs/cronet_http/pubspec.yaml b/pkgs/cronet_http/pubspec.yaml index 45e97af8ec..2b2386de63 100644 --- a/pkgs/cronet_http/pubspec.yaml +++ b/pkgs/cronet_http/pubspec.yaml @@ -1,5 +1,5 @@ name: cronet_http -version: 1.9.0 +version: 1.9.1-wip description: >- An Android Flutter plugin that provides access to the Cronet HTTP client. repository: https://github.com/dart-lang/http/tree/master/pkgs/cronet_http diff --git a/pkgs/cupertino_http/CHANGELOG.md b/pkgs/cupertino_http/CHANGELOG.md index ebfef8cf57..6bbaa368b6 100644 --- a/pkgs/cupertino_http/CHANGELOG.md +++ b/pkgs/cupertino_http/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.3-wip + +* Update example to fetch and display scores of package:cupertino_http from pub.dev. + ## 3.0.2 * Fix a [bug](https://github.com/dart-lang/http/issues/1929) where code diff --git a/pkgs/cupertino_http/README.md b/pkgs/cupertino_http/README.md index 6f066a58f6..6622828ac7 100644 --- a/pkgs/cupertino_http/README.md +++ b/pkgs/cupertino_http/README.md @@ -35,16 +35,15 @@ void main() async { if (Platform.isIOS || Platform.isMacOS) { final config = URLSessionConfiguration.ephemeralSessionConfiguration() ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) - ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + ..httpAdditionalHeaders = {'User-Agent': 'Package Client'}; httpClient = CupertinoClient.fromSessionConfiguration(config); } else { - httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); + httpClient = IOClient(HttpClient()..userAgent = 'Package Client'); } - final response = await httpClient.get(Uri.https( - 'www.googleapis.com', - '/books/v1/volumes', - {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); + final response = await httpClient.get( + Uri.https('pub.dev', '/api/packages/cupertino_http/score'), + ); httpClient.close(); } diff --git a/pkgs/cupertino_http/example/example.dart b/pkgs/cupertino_http/example/example.dart index ee930cf63e..6a9d44b5f6 100644 --- a/pkgs/cupertino_http/example/example.dart +++ b/pkgs/cupertino_http/example/example.dart @@ -1,26 +1,28 @@ +// Copyright (c) 2026, 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' as convert; import 'package:cupertino_http/cupertino_http.dart'; void main(List arguments) async { - // This example uses the Google Books API to search for books about http. - // https://developers.google.com/books/docs/overview - - // For a complete example of using `package:cupertino_http` in a Flutter - // application, see: - // https://github.com/dart-lang/http/tree/master/pkgs/flutter_http_example + // This example uses the pub.dev API to fetch details about + // package:cupertino_http. + // https://pub.dev/help/api final client = CupertinoClient.defaultSessionConfiguration(); - final url = Uri.https('www.googleapis.com', '/books/v1/volumes', { - 'q': '{http}', - }); + final url = Uri.https('pub.dev', '/api/packages/cupertino_http/score'); // Await the http get response, then decode the json-formatted response. final response = await client.get(url); if (response.statusCode == 200) { var jsonResponse = convert.jsonDecode(response.body) as Map; - var itemCount = jsonResponse['totalItems']; - print('Number of books about http: $itemCount.'); + var likes = jsonResponse['likeCount']; + var downloads = jsonResponse['downloadCount30Days']; + print('Information about package:cupertino_http:'); + print('- Likes: $likes'); + print('- 30-day downloads: $downloads'); } else { print('Request failed with status: ${response.statusCode}.'); } diff --git a/pkgs/cupertino_http/lib/cupertino_http.dart b/pkgs/cupertino_http/lib/cupertino_http.dart index c6c61ddb38..7a8cdf6dd2 100644 --- a/pkgs/cupertino_http/lib/cupertino_http.dart +++ b/pkgs/cupertino_http/lib/cupertino_http.dart @@ -19,14 +19,13 @@ /// ``` /// import 'dart:convert'; /// import 'dart:io'; -/// import 'dart:math'; /// /// import 'package:cupertino_http/cupertino_http.dart'; /// /// void main() async { /// var client = CupertinoClient.defaultSessionConfiguration(); /// final response = await client.get( -/// Uri.https('www.googleapis.com', '/books/v1/volumes', {'q': '{http}'})); +/// Uri.https('pub.dev', '/api/packages/cupertino_http/score')); /// if (response.statusCode != 200) { /// throw HttpException('bad response: ${response.statusCode}'); /// } @@ -34,11 +33,8 @@ /// final decodedResponse = /// jsonDecode(utf8.decode(response.bodyBytes)) as Map; /// -/// final itemCount = decodedResponse['totalItems']; -/// print('Number of books about http: $itemCount.'); -/// for (var i = 0; i < min(itemCount, 10); ++i) { -/// print(decodedResponse['items'][i]['volumeInfo']['title']); -/// } +/// final likes = decodedResponse['likeCount']; +/// print('Likes: $likes'); /// client.close(); /// } /// ``` @@ -55,15 +51,16 @@ /// if (Platform.isIOS || Platform.isMacOS) { /// final config = URLSessionConfiguration.ephemeralSessionConfiguration() /// ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) -/// ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; +/// ..httpAdditionalHeaders = {'User-Agent': 'Package Client'}; /// httpClient = CupertinoClient.fromSessionConfiguration(config); /// } else { -/// httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); +/// httpClient = IOClient( +/// HttpClient()..userAgent = 'Package Client'); /// } /// /// runApp(Provider( /// create: (_) => httpClient, -/// child: const BookSearchApp(), +/// child: const PackageDetailsApp(), /// dispose: (_, client) => client.close())); /// } /// ``` diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index a51fc83164..1eaf6b856d 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -1,5 +1,5 @@ name: cupertino_http -version: 3.0.2 +version: 3.0.3-wip description: >- A macOS/iOS Flutter plugin that provides access to the Foundation URL Loading System. diff --git a/pkgs/flutter_http_example/.gitignore b/pkgs/flutter_http_example/.gitignore index 24476c5d1e..6c319542b3 100644 --- a/pkgs/flutter_http_example/.gitignore +++ b/pkgs/flutter_http_example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/pkgs/flutter_http_example/android/build.gradle b/pkgs/flutter_http_example/android/build.gradle index f7eb7f63ce..bc157bd1a1 100644 --- a/pkgs/flutter_http_example/android/build.gradle +++ b/pkgs/flutter_http_example/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/pkgs/flutter_http_example/android/gradle.properties b/pkgs/flutter_http_example/android/gradle.properties index 94adc3a3f9..eac5ff93b0 100644 --- a/pkgs/flutter_http_example/android/gradle.properties +++ b/pkgs/flutter_http_example/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/pkgs/flutter_http_example/android/settings.gradle b/pkgs/flutter_http_example/android/settings.gradle index 55c4ca8b10..4f520718dc 100644 --- a/pkgs/flutter_http_example/android/settings.gradle +++ b/pkgs/flutter_http_example/android/settings.gradle @@ -5,16 +5,21 @@ pluginManagement { def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() + }() - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + repositories { + google() + mavenCentral() + gradlePluginPortal() } } -include ":app" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false +} -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ":app" diff --git a/pkgs/flutter_http_example/lib/book.dart b/pkgs/flutter_http_example/lib/book.dart deleted file mode 100644 index b47ca9e67e..0000000000 --- a/pkgs/flutter_http_example/lib/book.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2022, 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 listFromJson(Map json) { - final books = []; - - if (json['items'] case final List items) { - for (final item in items) { - if (item case {'volumeInfo': final Map 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; - } -} diff --git a/pkgs/flutter_http_example/lib/http_client_factory.dart b/pkgs/flutter_http_example/lib/http_client_factory.dart index 51109e520c..0745ea6677 100644 --- a/pkgs/flutter_http_example/lib/http_client_factory.dart +++ b/pkgs/flutter_http_example/lib/http_client_factory.dart @@ -18,14 +18,14 @@ Client httpClient() { final engine = CronetEngine.build( cacheMode: CacheMode.memory, cacheMaxSize: _maxCacheSize, - userAgent: 'Book Agent'); + userAgent: 'Package Client'); return CronetClient.fromCronetEngine(engine); } if (Platform.isIOS || Platform.isMacOS) { final config = URLSessionConfiguration.ephemeralSessionConfiguration() ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) - ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + ..httpAdditionalHeaders = {'User-Agent': 'Package Client'}; return CupertinoClient.fromSessionConfiguration(config); } - return IOClient(HttpClient()..userAgent = 'Book Agent'); + return IOClient(HttpClient()..userAgent = 'Package Client'); } diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 899105b227..aa608c3419 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -9,9 +9,9 @@ import 'package:http/http.dart'; import 'package:http_image_provider/http_image_provider.dart'; import 'package:provider/provider.dart'; -import 'book.dart'; import 'http_client_factory.dart' if (dart.library.js_interop) 'http_client_factory_web.dart' as http_factory; +import 'package.dart'; void main() { runApp(Provider( @@ -24,31 +24,35 @@ void main() { // - allow caching of fetched URLs // - allow connections to be persisted create: (_) => http_factory.httpClient(), - child: const BookSearchApp(), + child: const PackageSearchApp(), dispose: (_, client) => client.close())); } -class BookSearchApp extends StatelessWidget { - const BookSearchApp({super.key}); +class PackageSearchApp extends StatelessWidget { + const PackageSearchApp({super.key}); @override - Widget build(BuildContext context) => const MaterialApp( - // Remove the debug banner. + Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, - title: 'Book Search', - home: HomePage(), + title: 'Dart Package Search', + theme: ThemeData(useMaterial3: true), + darkTheme: ThemeData.dark(useMaterial3: true), + home: const SearchPage(), ); } -class HomePage extends StatefulWidget { - const HomePage({super.key}); +class SearchPage extends StatefulWidget { + const SearchPage({super.key}); @override - State createState() => _HomePageState(); + State createState() => _SearchPageState(); } -class _HomePageState extends State { - List? _books; +class _SearchPageState extends State { + List? _allPackages; + List? _matchingNames; + bool _loadingPackages = false; + String? _errorMessage; String? _lastQuery; late Client _client; @@ -56,65 +60,121 @@ class _HomePageState extends State { void initState() { super.initState(); _client = context.read(); + _loadPackageList(); } - // Get the list of books matching `query`. - // The `get` call will automatically use the `client` configured in `main`. - Future> _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); + Future _loadPackageList() async { + setState(() { + _loadingPackages = true; + _errorMessage = null; + }); + try { + final response = await _client.get( + Uri.https('pub.dev', '/api/package-name-completion-data'), + ); + if (response.statusCode == 200) { + final json = + jsonDecode(utf8.decode(response.bodyBytes)) as Map; + final packagesList = (json['packages'] as List).cast() + ..sort((a, b) => a.compareTo(b)); + setState(() { + _allPackages = packagesList; + _loadingPackages = false; + _runSearch(_lastQuery ?? ''); + }); + } else { + setState(() { + _errorMessage = + 'Failed to load package list: Status ${response.statusCode}'; + _loadingPackages = false; + }); + } + } catch (e) { + setState(() { + _errorMessage = 'Failed to load package list: $e'; + _loadingPackages = false; + }); + } } - void _runSearch(String query) async { + void _runSearch(String query) { _lastQuery = query; + final allPackages = _allPackages; + if (allPackages == null) return; + if (query.isEmpty) { - setState(() { - _books = null; - }); + setState(() => _matchingNames = allPackages); 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; + final lowercaseQuery = query.toLowerCase(); setState(() { - _books = books; + _matchingNames = allPackages + .where((name) => name.toLowerCase().contains(lowercaseQuery)) + .toList(); }); } @override Widget build(BuildContext context) { - 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)); + Widget body; + if (_loadingPackages) { + body = const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading package database...'), + ], + ), + ); + } else if (_errorMessage != null) { + body = Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _errorMessage!, + style: const TextStyle(color: Colors.redAccent), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadPackageList, + child: const Text('Retry'), + ), + ], + ), + ); + } else if (_matchingNames == null) { + body = const Center(child: Text('Loading...')); + } else { + body = PackageList( + packageNames: _matchingNames!, + client: _client, + ); + } return Scaffold( - appBar: AppBar(title: const Text('Book Search')), + appBar: AppBar( + title: const Text('Dart Package Search'), + centerTitle: true, + ), body: Padding( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.all(16), child: Column( children: [ - const SizedBox(height: 20), TextField( onChanged: _runSearch, decoration: const InputDecoration( - labelText: 'Search', - suffixIcon: Icon(Icons.search), + labelText: 'Search packages', + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), ), ), - const SizedBox(height: 20), - Expanded(child: searchResult), + const SizedBox(height: 16), + Expanded(child: body), ], ), ), @@ -122,27 +182,164 @@ class _HomePageState extends State { } } -class BookList extends StatefulWidget { - final List books; - const BookList(this.books, {super.key}); +class PackageList extends StatelessWidget { + final List packageNames; + final Client client; + + const PackageList({ + required this.packageNames, + required this.client, + super.key, + }); + + @override + Widget build(BuildContext context) => ListView.builder( + itemCount: packageNames.length, + itemBuilder: (context, index) { + final name = packageNames[index]; + return PackageCard( + key: ValueKey(name), + name: name, + client: client, + ); + }, + ); +} + +class PackageCard extends StatefulWidget { + final String name; + final Client client; + + const PackageCard({ + required this.name, + required this.client, + super.key, + }); @override - State createState() => _BookListState(); + State createState() => _PackageCardState(); } -class _BookListState extends State { +class _PackageCardState extends State { + late Future _packageFuture; + @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: HttpImageProvider( - widget.books[index].imageUrl.replace(scheme: 'https'), - client: context.read())), - title: Text(widget.books[index].title), - subtitle: Text(widget.books[index].description), + void initState() { + super.initState(); + _packageFuture = _fetchPackageInfo(widget.name, widget.client); + } + + Future _fetchPackageInfo(String name, Client client) async { + final results = await Future.wait([ + client.get(Uri.https('pub.dev', '/api/packages/$name/score')), + client.get(Uri.https('pub.dev', '/api/packages/$name/publisher')), + ]); + + final scoreResponse = results[0]; + final publisherResponse = results[1]; + + if (scoreResponse.statusCode == 200 && + publisherResponse.statusCode == 200) { + final scoreJson = jsonDecode(utf8.decode(scoreResponse.bodyBytes)) + as Map; + final publisherJson = jsonDecode(utf8.decode(publisherResponse.bodyBytes)) + as Map; + final publisherId = publisherJson['publisherId'] as String?; + return Package.fromJson(name, scoreJson, publisherId: publisherId); + } else { + throw Exception('Failed to load package info'); + } + } + + @override + Widget build(BuildContext context) => Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Padding( + padding: const EdgeInsets.all(16), + child: FutureBuilder( + future: _packageFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + const LinearProgressIndicator(), + ], + ); + } + + if (snapshot.hasError) { + return Text('Error loading info for ${widget.name}'); + } + + final package = snapshot.data!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + package.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + if (package.publisherId != null) ...[ + const SizedBox(height: 6), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image( + // Image.network does not allow you to provide your + // own `http.Client` so use `HttpImageProvider`. + image: HttpImageProvider( + Uri.https('www.google.com', '/s2/favicons', { + 'sz': '64', + 'domain': package.publisherId, + }), + client: widget.client, + ), + width: 16, + height: 16, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.public, size: 16), + ), + ), + const SizedBox(width: 6), + Text( + package.publisherId!, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + const SizedBox(height: 12), + Row( + children: [ + const Icon(Icons.thumb_up_outlined, size: 16), + const SizedBox(width: 6), + Text('${package.likes} Likes'), + const SizedBox(width: 24), + const Icon(Icons.download_outlined, size: 16), + const SizedBox(width: 6), + Text('${package.downloads} Downloads'), + ], + ), + ], + ); + }, ), ), ); diff --git a/pkgs/flutter_http_example/lib/package.dart b/pkgs/flutter_http_example/lib/package.dart new file mode 100644 index 0000000000..b04d0d5320 --- /dev/null +++ b/pkgs/flutter_http_example/lib/package.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2026, 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 Package { + final String name; + final int likes; + final int downloads; + final String? publisherId; + + Package({ + required this.name, + required this.likes, + required this.downloads, + required this.publisherId, + }); + + factory Package.fromJson( + String name, + Map json, { + required String? publisherId, + }) => + Package( + name: name, + likes: json['likeCount'] as int? ?? 0, + downloads: json['downloadCount30Days'] as int? ?? 0, + publisherId: publisherId, + ); +} diff --git a/pkgs/flutter_http_example/macos/Runner.xcodeproj/project.pbxproj b/pkgs/flutter_http_example/macos/Runner.xcodeproj/project.pbxproj index 02b4091aa8..816bf2f1bf 100644 --- a/pkgs/flutter_http_example/macos/Runner.xcodeproj/project.pbxproj +++ b/pkgs/flutter_http_example/macos/Runner.xcodeproj/project.pbxproj @@ -240,7 +240,6 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 07BDB66C6CC1CF49E12920D9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -344,23 +343,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 07BDB66C6CC1CF49E12920D9 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/pkgs/flutter_http_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/pkgs/flutter_http_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a88e082977..a33828e458 100644 --- a/pkgs/flutter_http_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/pkgs/flutter_http_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/pkgs/flutter_http_example/macos/Runner/AppDelegate.swift b/pkgs/flutter_http_example/macos/Runner/AppDelegate.swift index 8e02df2888..b3c1761412 100644 --- a/pkgs/flutter_http_example/macos/Runner/AppDelegate.swift +++ b/pkgs/flutter_http_example/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/pkgs/flutter_http_example/pubspec.yaml b/pkgs/flutter_http_example/pubspec.yaml index 92844828d2..5229b8de57 100644 --- a/pkgs/flutter_http_example/pubspec.yaml +++ b/pkgs/flutter_http_example/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: cronet_http: ^1.0.0 - cupertino_http: ^2.0.0 + cupertino_http: ^3.0.2 cupertino_icons: ^1.0.2 fetch_client: ^1.0.2 flutter: diff --git a/pkgs/flutter_http_example/test/widget_test.dart b/pkgs/flutter_http_example/test/widget_test.dart index f890a1bb11..d7adbc9241 100644 --- a/pkgs/flutter_http_example/test/widget_test.dart +++ b/pkgs/flutter_http_example/test/widget_test.dart @@ -2,8 +2,6 @@ // 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 'package:flutter/material.dart'; import 'package:flutter_http_example/main.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -11,64 +9,278 @@ import 'package:http/http.dart'; import 'package:http/testing.dart'; import 'package:provider/provider.dart'; -const _singleBookResponse = ''' +const _completionResponse = ''' { - "items": [ - { - "volumeInfo": { - "title": "Flutter Cookbook", - "description": "Write, test, and publish your web, desktop...", - "imageLinks": { - "smallThumbnail": "http://thumbnailurl/" - } - } - } + "packages": [ + "http", + "http_parser", + "shared_preferences" ] } '''; -final _dummyPngImage = base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmM' - 'IQAAAABJRU5ErkJggg==', -); +const _httpScoreResponse = ''' +{ + "likeCount": 8458, + "downloadCount30Days": 9436929 +} +'''; + +const _httpParserScoreResponse = ''' +{ + "likeCount": 150, + "downloadCount30Days": 500000 +} +'''; + +const _sharedPrefsScoreResponse = ''' +{ + "likeCount": 3200, + "downloadCount30Days": 4000000 +} +'''; + +const _transparentPng = [ + 137, + 80, + 78, + 71, + 13, + 10, + 26, + 10, + 0, + 0, + 0, + 13, + 73, + 72, + 68, + 82, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 8, + 6, + 0, + 0, + 0, + 31, + 21, + 196, + 137, + 0, + 0, + 0, + 11, + 73, + 68, + 65, + 84, + 120, + 156, + 99, + 96, + 0, + 0, + 0, + 2, + 0, + 1, + 226, + 33, + 188, + 51, + 0, + 0, + 0, + 0, + 73, + 69, + 78, + 68, + 174, + 66, + 96, + 130 +]; void main() { Widget app(Client client) => Provider( create: (_) => client, - child: const BookSearchApp(), + child: const PackageSearchApp(), dispose: (_, client) => client.close()); - testWidgets('test initial load', (WidgetTester tester) async { - final mockClient = MockClient( - (request) async => throw StateError('unexpected HTTP request')); + testWidgets('test initial load displays all packages and loads scores', + (WidgetTester tester) async { + final mockClient = MockClient((request) async { + if (request.url.path == '/api/package-name-completion-data') { + return Response(_completionResponse, 200); + } else if (request.url.path == '/api/packages/http/score') { + return Response(_httpScoreResponse, 200); + } else if (request.url.path == '/api/packages/http/publisher') { + return Response('{"publisherId": "dart.dev"}', 200); + } else if (request.url.path == '/api/packages/http_parser/score') { + return Response(_httpParserScoreResponse, 200); + } else if (request.url.path == '/api/packages/http_parser/publisher') { + return Response('{"publisherId": "dart.dev"}', 200); + } else if (request.url.path == '/api/packages/shared_preferences/score') { + return Response(_sharedPrefsScoreResponse, 200); + } else if (request.url.path == + '/api/packages/shared_preferences/publisher') { + return Response('{"publisherId": "flutter.dev"}', 200); + } else if (request.url.path == '/s2/favicons') { + return Response.bytes(_transparentPng, 200); + } + throw StateError('unexpected HTTP request: ${request.url.path}'); + }); + + await tester.pumpWidget(app(mockClient)); + // Wait for the completion data and all visible scores to load. + await tester.pumpAndSettle(); + + // Verify all package names are shown. + expect( + find.descendant( + of: find.byType(PackageList), + matching: find.text('http'), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(PackageList), + matching: find.text('http_parser'), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(PackageList), + matching: find.text('shared_preferences'), + ), + findsOneWidget, + ); + + // Verify publisher domains are shown. + expect(find.text('dart.dev'), findsNWidgets(2)); + expect(find.text('flutter.dev'), findsOneWidget); + + // Verify scores are loaded and displayed. + expect(find.text('8458 Likes'), findsOneWidget); + expect(find.text('9436929 Downloads'), findsOneWidget); + }); + + testWidgets('test search filters results', (WidgetTester tester) async { + final mockClient = MockClient((request) async { + if (request.url.path == '/api/package-name-completion-data') { + return Response(_completionResponse, 200); + } else if (request.url.path == '/api/packages/http/score') { + return Response(_httpScoreResponse, 200); + } else if (request.url.path == '/api/packages/http/publisher') { + return Response('{"publisherId": "dart.dev"}', 200); + } else if (request.url.path == '/api/packages/http_parser/score') { + return Response(_httpParserScoreResponse, 200); + } else if (request.url.path == '/api/packages/http_parser/publisher') { + return Response('{"publisherId": "dart.dev"}', 200); + } else if (request.url.path == '/api/packages/shared_preferences/score') { + return Response(_sharedPrefsScoreResponse, 200); + } else if (request.url.path == + '/api/packages/shared_preferences/publisher') { + return Response('{"publisherId": "flutter.dev"}', 200); + } else if (request.url.path == '/s2/favicons') { + return Response.bytes(_transparentPng, 200); + } + return Response('', 404); + }); await tester.pumpWidget(app(mockClient)); + // Wait for initial load to finish. + await tester.pumpAndSettle(); + + // Search for 'http' + await tester.enterText(find.byType(TextField), 'http'); + await tester.pumpAndSettle(); + + // The matching package names should be present. + expect( + find.descendant( + of: find.byType(PackageList), + matching: find.text('http'), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(PackageList), + matching: find.text('http_parser'), + ), + findsOneWidget, + ); - expect(find.text('Please enter a query'), findsOneWidget); + // The non-matching package should not be present. + expect( + find.descendant( + of: find.byType(PackageList), + matching: find.text('shared_preferences'), + ), + findsNothing, + ); }); - testWidgets('test search with one result', (WidgetTester tester) async { + testWidgets('test load package list error displays error and retry', + (WidgetTester tester) async { + var requestCount = 0; final mockClient = MockClient((request) async { - if (request.url.path == '/books/v1/volumes' && - request.url.queryParameters['q'] == 'Flutter') { - return Response(_singleBookResponse, 200); - } else if (request.url == Uri.https('thumbnailurl', '/')) { - return Response.bytes(_dummyPngImage, 200, - headers: const {'Content-Type': 'image/png'}); + requestCount++; + if (requestCount == 1) { + return Response('Internal Server Error', 500); + } + if (request.url.path == '/api/package-name-completion-data') { + return Response(_completionResponse, 200); + } else if (request.url.path == '/api/packages/http/score') { + return Response(_httpScoreResponse, 200); + } else if (request.url.path == '/api/packages/http/publisher') { + return Response('{"publisherId": "dart.dev"}', 200); + } else if (request.url.path == '/api/packages/http_parser/score') { + return Response(_httpParserScoreResponse, 200); + } else if (request.url.path == '/api/packages/http_parser/publisher') { + return Response('{"publisherId": "dart.dev"}', 200); + } else if (request.url.path == '/api/packages/shared_preferences/score') { + return Response(_sharedPrefsScoreResponse, 200); + } else if (request.url.path == + '/api/packages/shared_preferences/publisher') { + return Response('{"publisherId": "flutter.dev"}', 200); + } else if (request.url.path == '/s2/favicons') { + return Response.bytes(_transparentPng, 200); } return Response('', 404); }); await tester.pumpWidget(app(mockClient)); - await tester.enterText(find.byType(TextField), 'Flutter'); - await tester.pump(); + await tester.pumpAndSettle(); + + // Verify error message and retry button are shown. + expect( + find.text('Failed to load package list: Status 500'), findsOneWidget); + expect(find.byType(ElevatedButton), findsOneWidget); + + // Tap retry. + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); - // The book title. - expect(find.text('Flutter Cookbook'), findsOneWidget); - // The book description. + // Verify package list loads successfully after retry. expect( - find.text('Write, test, and publish your web, desktop...', - skipOffstage: false), - findsOneWidget); + find.descendant( + of: find.byType(PackageList), + matching: find.text('http'), + ), + findsOneWidget, + ); }); } diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 43ebb29f0b..1780bd02f4 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.7.0-wip +* Update example to fetch and display scores of package:http from pub.dev. * Add `BrowserCredentialsMode` to support the `omit` browser fetch credentials mode. Deprecate `withCredentials`. * Clarified the behavior of response headers in API documentation comments. diff --git a/pkgs/http/example/main.dart b/pkgs/http/example/main.dart index 34149188ba..a360c3a321 100644 --- a/pkgs/http/example/main.dart +++ b/pkgs/http/example/main.dart @@ -1,21 +1,29 @@ +// Copyright (c) 2026, 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' as convert; import 'package:http/http.dart' as http; void main(List arguments) async { - // This example uses the Google Books API to search for books about http. - // https://developers.google.com/books/docs/overview - var url = - Uri.https('www.googleapis.com', '/books/v1/volumes', {'q': '{http}'}); + // This example uses the pub.dev API to fetch details about package:http. + // See https://pub.dev/help/api + final client = http.Client(); + var url = Uri.https('pub.dev', '/api/packages/http/score'); // Await the http get response, then decode the json-formatted response. - var response = await http.get(url); + var response = await client.get(url); if (response.statusCode == 200) { var jsonResponse = convert.jsonDecode(response.body) as Map; - var itemCount = jsonResponse['totalItems']; - print('Number of books about http: $itemCount.'); + var likes = jsonResponse['likeCount']; + var downloads = jsonResponse['downloadCount30Days']; + print('Information about package:http:'); + print('- Likes: $likes'); + print('- 30-day downloads: $downloads'); } else { print('Request failed with status: ${response.statusCode}.'); } + client.close(); } diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart index a00ef9dd03..01802654fb 100644 --- a/pkgs/http/lib/src/io_client.dart +++ b/pkgs/http/lib/src/io_client.dart @@ -94,7 +94,7 @@ class IOClient extends BaseClient { /// For example: /// ```dart /// final httpClient = HttpClient() - /// ..userAgent = 'Book Agent' + /// ..userAgent = 'Package Client' /// ..idleTimeout = const Duration(seconds: 5); /// final client = IOClient(httpClient); /// ``` diff --git a/pkgs/ok_http/CHANGELOG.md b/pkgs/ok_http/CHANGELOG.md index f3d011414f..4a174d2eef 100644 --- a/pkgs/ok_http/CHANGELOG.md +++ b/pkgs/ok_http/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.1.1-wip +- Update example to fetch and display scores of package:ok_http from pub.dev. - `OkHttpClient` now receives an `OkHttpClientConfiguration` to configure the client on a per-call basis. - `OkHttpClient` supports setting four types of timeouts: [`connectTimeout`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/-builder/connect-timeout.html), [`readTimeout`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/-builder/read-timeout.html), [`writeTimeout`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/-builder/write-timeout.html), and [`callTimeout`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/-builder/call-timeout.html), using the `OkHttpClientConfiguration`. - Upgrade to `jni` 0.14.0 diff --git a/pkgs/ok_http/example/lib/book.dart b/pkgs/ok_http/example/lib/book.dart deleted file mode 100644 index 4954d2509b..0000000000 --- a/pkgs/ok_http/example/lib/book.dart +++ /dev/null @@ -1,32 +0,0 @@ -// 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 listFromJson(Map json) { - final books = []; - - if (json['items'] case final List items) { - for (final item in items) { - if (item case {'volumeInfo': final Map 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; - } -} diff --git a/pkgs/ok_http/example/lib/main.dart b/pkgs/ok_http/example/lib/main.dart index e8177895ec..36d35b0fa3 100644 --- a/pkgs/ok_http/example/lib/main.dart +++ b/pkgs/ok_http/example/lib/main.dart @@ -8,141 +8,85 @@ import 'dart:io'; import 'package:flutter/material.dart'; 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() { final Client httpClient; if (Platform.isAndroid) { httpClient = OkHttpClient(); } else { - httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); + httpClient = IOClient(HttpClient()..userAgent = 'Package Client'); } runApp(Provider( create: (_) => httpClient, - child: const BookSearchApp(), + child: const PackageDetailsApp(), dispose: (_, client) => client.close())); } -class BookSearchApp extends StatelessWidget { - const BookSearchApp({super.key}); +class PackageDetailsApp extends StatelessWidget { + const PackageDetailsApp({super.key}); @override Widget build(BuildContext context) => const MaterialApp( - // Remove the debug banner. debugShowCheckedModeBanner: false, - title: 'Book Search', - home: HomePage(), + title: 'Package Details', + home: PackageDetailsPage(), ); } -class HomePage extends StatefulWidget { - const HomePage({super.key}); +class PackageDetailsPage extends StatefulWidget { + const PackageDetailsPage({super.key}); @override - State createState() => _HomePageState(); + State createState() => _PackageDetailsPageState(); } -class _HomePageState extends State { - List? _books; - String? _lastQuery; - late Client _client; +class _PackageDetailsPageState extends State { + String _output = 'Loading...'; @override void initState() { super.initState(); - _client = context.read(); - } - - // Get the list of books matching `query`. - // The `get` call will automatically use the `client` configured in `main`. - Future> _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); + _fetchPackageInfo(); } - void _runSearch(String query) async { - _lastQuery = query; - if (query.isEmpty) { + void _fetchPackageInfo() async { + final client = context.read(); + try { + final response = await client.get( + Uri.https('pub.dev', '/api/packages/ok_http/score'), + ); + if (response.statusCode == 200) { + final json = + jsonDecode(utf8.decode(response.bodyBytes)) as Map; + setState(() { + _output = 'Information about package:ok_http:\n' + '- Likes: ${json['likeCount']}\n' + '- 30-day downloads: ${json['downloadCount30Days']}'; + }); + } else { + setState(() { + _output = 'Request failed with status: ${response.statusCode}.'; + }); + } + } catch (e) { setState(() { - _books = null; + _output = 'Request failed: $e'; }); - 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) { - 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), - ), + Widget build(BuildContext context) => Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + _output, + style: const TextStyle(fontSize: 18, fontFamily: 'monospace'), ), - const SizedBox(height: 20), - Expanded(child: searchResult), - ], - ), - ), - ); - } -} - -class BookList extends StatefulWidget { - final List books; - const BookList(this.books, {super.key}); - - @override - State createState() => _BookListState(); -} - -class _BookListState extends State { - @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: HttpImageProvider( - widget.books[index].imageUrl.replace(scheme: 'https'), - client: context.read())), - title: Text(widget.books[index].title), - subtitle: Text(widget.books[index].description), ), ), ); diff --git a/pkgs/ok_http/example/pubspec.yaml b/pkgs/ok_http/example/pubspec.yaml index 319c5faf5d..dc44174741 100644 --- a/pkgs/ok_http/example/pubspec.yaml +++ b/pkgs/ok_http/example/pubspec.yaml @@ -8,11 +8,9 @@ environment: sdk: ">=3.4.1 <4.0.0" dependencies: - cupertino_icons: ^1.0.6 flutter: sdk: flutter http: ^1.0.0 - http_image_provider: ^1.0.0 ok_http: path: ../ provider: ^6.1.1 diff --git a/pkgs/ok_http/lib/ok_http.dart b/pkgs/ok_http/lib/ok_http.dart index 9670b18753..85998dc775 100644 --- a/pkgs/ok_http/lib/ok_http.dart +++ b/pkgs/ok_http/lib/ok_http.dart @@ -13,7 +13,7 @@ /// void main() async { /// var client = OkHttpClient(); /// final response = await client.get( -/// Uri.https('www.googleapis.com', '/books/v1/volumes', {'q': '{http}'})); +/// Uri.https('pub.dev', '/api/packages/ok_http/score')); /// if (response.statusCode != 200) { /// throw HttpException('bad response: ${response.statusCode}'); /// } @@ -21,11 +21,8 @@ /// final decodedResponse = /// jsonDecode(utf8.decode(response.bodyBytes)) as Map; /// -/// final itemCount = decodedResponse['totalItems']; -/// print('Number of books about http: $itemCount.'); -/// for (var i = 0; i < min(itemCount, 10); ++i) { -/// print(decodedResponse['items'][i]['volumeInfo']['title']); -/// } +/// final likes = decodedResponse['likeCount']; +/// print('Likes: $likes'); /// } /// ``` /// diff --git a/pkgs/ok_http/lib/src/ok_http_client.dart b/pkgs/ok_http/lib/src/ok_http_client.dart index e9114b4893..7a891cf2a4 100644 --- a/pkgs/ok_http/lib/src/ok_http_client.dart +++ b/pkgs/ok_http/lib/src/ok_http_client.dart @@ -198,7 +198,7 @@ Future choosePrivateKeyAlias({ /// void main() async { /// var client = OkHttpClient(); /// final response = await client.get( -/// Uri.https('www.googleapis.com', '/books/v1/volumes', {'q': '{http}'})); +/// Uri.https('pub.dev', '/api/packages/ok_http/score')); /// if (response.statusCode != 200) { /// throw HttpException('bad response: ${response.statusCode}'); /// } @@ -206,11 +206,8 @@ Future choosePrivateKeyAlias({ /// final decodedResponse = /// jsonDecode(utf8.decode(response.bodyBytes)) as Map; /// -/// final itemCount = decodedResponse['totalItems']; -/// print('Number of books about http: $itemCount.'); -/// for (var i = 0; i < min(itemCount, 10); ++i) { -/// print(decodedResponse['items'][i]['volumeInfo']['title']); -/// } +/// final likes = decodedResponse['likeCount']; +/// print('Likes: $likes'); /// } /// ``` class OkHttpClient extends BaseClient {