From 14502af14aa83ec709ad14fc0275b29df40d3d04 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 20 Jul 2026 16:41:06 -0700 Subject: [PATCH 01/17] feat: use package search --- pkgs/flutter_http_example/.gitignore | 2 + pkgs/flutter_http_example/lib/book.dart | 32 --- pkgs/flutter_http_example/lib/main.dart | 269 +++++++++++++----- pkgs/flutter_http_example/lib/package.dart | 21 ++ .../xcshareddata/xcschemes/Runner.xcscheme | 1 + .../macos/Runner/AppDelegate.swift | 4 + .../test/widget_test.dart | 145 +++++++--- 7 files changed, 330 insertions(+), 144 deletions(-) delete mode 100644 pkgs/flutter_http_example/lib/book.dart create mode 100644 pkgs/flutter_http_example/lib/package.dart 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/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/main.dart b/pkgs/flutter_http_example/lib/main.dart index 899105b227..6187cf101e 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -6,36 +6,26 @@ import 'dart:convert'; import 'package:flutter/material.dart'; 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( - // `Provider` calls its `create` argument once when a `Client` is - // first requested (through `BuildContext.read()`) and uses that - // same instance for all future requests. - // - // Reusing the same `Client` may: - // - reduce memory usage - // - 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. debugShowCheckedModeBanner: false, - title: 'Book Search', + title: 'Pub.dev Package Search', home: HomePage(), ); } @@ -48,7 +38,9 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - List? _books; + List? _allPackages; + List? _matchingNames; + bool _loadingPackages = false; String? _lastQuery; late Client _client; @@ -56,94 +48,221 @@ 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); + 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 ?? ''); + }); + } + } catch (_) { + setState(() => _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: CircularProgressIndicator()); + } else if (_matchingNames == null) { + body = const Center(child: Text('Loading...')); + } else { + body = PackageTable( + packageNames: _matchingNames!, + client: _client, + ); + } return Scaffold( - appBar: AppBar(title: const Text('Book Search')), - body: Padding( - padding: const EdgeInsets.all(10), - child: Column( - children: [ - const SizedBox(height: 20), - TextField( + appBar: AppBar(title: const Text('Pub.dev Package Search')), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( onChanged: _runSearch, - decoration: const InputDecoration( - labelText: 'Search', - suffixIcon: Icon(Icons.search), - ), + decoration: const InputDecoration(labelText: 'Search packages'), ), - const SizedBox(height: 20), - Expanded(child: searchResult), - ], - ), + ), + Expanded(child: body), + ], ), ); } } -class BookList extends StatefulWidget { - final List books; - const BookList(this.books, {super.key}); +class PackageTable extends StatelessWidget { + final List packageNames; + final Client client; + + const PackageTable({ + required this.packageNames, + required this.client, + super.key, + }); @override - State createState() => _BookListState(); + Widget build(BuildContext context) => Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + flex: 4, + child: Text( + 'Package Name', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + Expanded( + flex: 2, + child: Text( + 'Likes', + style: TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 3, + child: Text( + '30-day Downloads', + style: TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: packageNames.length, + itemBuilder: (context, index) { + final name = packageNames[index]; + return PackageRow( + key: ValueKey(name), + name: name, + client: client, + ); + }, + ), + ), + ], + ); } -class _BookListState extends State { +class PackageRow extends StatefulWidget { + final String name; + final Client client; + + const PackageRow({ + required this.name, + required this.client, + super.key, + }); + @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), - ), + State createState() => _PackageRowState(); +} + +class _PackageRowState extends State { + late Future _scoreFuture; + + @override + void initState() { + super.initState(); + _scoreFuture = _fetchScore(widget.name, widget.client); + } + + Future _fetchScore(String name, Client client) async { + final response = await client.get( + Uri.https('pub.dev', '/api/packages/$name/score'), + ); + if (response.statusCode == 200) { + final json = jsonDecode(utf8.decode(response.bodyBytes)) + as Map; + return Package.fromJson(name, json); + } else { + throw Exception('Failed to load score'); + } + } + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + flex: 4, + child: Text(widget.name), + ), + Expanded( + flex: 5, + child: FutureBuilder( + future: _scoreFuture, + builder: (context, snapshot) { + final package = snapshot.data; + final likesText = + package != null ? package.likes.toString() : '...'; + final downloadsText = + package != null ? package.downloads.toString() : '...'; + + return Row( + children: [ + Expanded( + flex: 2, + child: Text( + likesText, + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 3, + child: Text( + downloadsText, + textAlign: TextAlign.right, + ), + ), + ], + ); + }, + ), + ), + ], ), ); } diff --git a/pkgs/flutter_http_example/lib/package.dart b/pkgs/flutter_http_example/lib/package.dart new file mode 100644 index 0000000000..9eaa98b590 --- /dev/null +++ b/pkgs/flutter_http_example/lib/package.dart @@ -0,0 +1,21 @@ +// 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; + + Package({ + required this.name, + required this.likes, + required this.downloads, + }); + + factory Package.fromJson(String name, Map json) => Package( + name: name, + likes: json['likeCount'] as int? ?? 0, + downloads: json['downloadCount30Days'] as int? ?? 0, + ); +} 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/test/widget_test.dart b/pkgs/flutter_http_example/test/widget_test.dart index f890a1bb11..83c6a2cdda 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,137 @@ 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 +} +'''; 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_parser/score') { + return Response(_httpParserScoreResponse, 200); + } else if (request.url.path == '/api/packages/shared_preferences/score') { + return Response(_sharedPrefsScoreResponse, 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(PackageTable), + matching: find.text('http'), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(PackageTable), + matching: find.text('http_parser'), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(PackageTable), + matching: find.text('shared_preferences'), + ), + findsOneWidget, + ); - expect(find.text('Please enter a query'), findsOneWidget); + // Verify scores are loaded and displayed. + expect(find.text('8458'), findsOneWidget); + expect(find.text('9436929'), findsOneWidget); + expect(find.text('3200'), findsOneWidget); + expect(find.text('4000000'), findsOneWidget); }); - testWidgets('test search with one result', (WidgetTester tester) async { + testWidgets('test search filters results', (WidgetTester tester) async { 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'}); + 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_parser/score') { + return Response(_httpParserScoreResponse, 200); + } else if (request.url.path == '/api/packages/shared_preferences/score') { + return Response(_sharedPrefsScoreResponse, 200); } return Response('', 404); }); await tester.pumpWidget(app(mockClient)); - await tester.enterText(find.byType(TextField), 'Flutter'); - await tester.pump(); + // 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(PackageTable), + matching: find.text('http'), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(PackageTable), + matching: find.text('http_parser'), + ), + findsOneWidget, + ); - // The book title. - expect(find.text('Flutter Cookbook'), findsOneWidget); - // The book description. + // The non-matching package should not be present. expect( - find.text('Write, test, and publish your web, desktop...', - skipOffstage: false), - findsOneWidget); + find.descendant( + of: find.byType(PackageTable), + matching: find.text('shared_preferences'), + ), + findsNothing, + ); }); } From 465edc7b9fd865d6d38d12c904ddab140ee060b1 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 20 Jul 2026 16:45:09 -0700 Subject: [PATCH 02/17] short --- pkgs/flutter_http_example/lib/main.dart | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 6187cf101e..30d2924c9f 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -58,10 +58,9 @@ class _HomePageState extends State { 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() + 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; @@ -142,26 +141,18 @@ class PackageTable extends StatelessWidget { child: Row( children: [ Expanded( - flex: 4, child: Text( 'Package Name', - style: TextStyle(fontWeight: FontWeight.bold), ), ), Expanded( - flex: 2, child: Text( 'Likes', - style: TextStyle(fontWeight: FontWeight.bold), - textAlign: TextAlign.right, ), ), Expanded( - flex: 3, child: Text( '30-day Downloads', - style: TextStyle(fontWeight: FontWeight.bold), - textAlign: TextAlign.right, ), ), ], @@ -213,8 +204,8 @@ class _PackageRowState extends State { Uri.https('pub.dev', '/api/packages/$name/score'), ); if (response.statusCode == 200) { - final json = jsonDecode(utf8.decode(response.bodyBytes)) - as Map; + final json = + jsonDecode(utf8.decode(response.bodyBytes)) as Map; return Package.fromJson(name, json); } else { throw Exception('Failed to load score'); @@ -227,11 +218,9 @@ class _PackageRowState extends State { child: Row( children: [ Expanded( - flex: 4, child: Text(widget.name), ), Expanded( - flex: 5, child: FutureBuilder( future: _scoreFuture, builder: (context, snapshot) { @@ -244,17 +233,13 @@ class _PackageRowState extends State { return Row( children: [ Expanded( - flex: 2, child: Text( likesText, - textAlign: TextAlign.right, ), ), Expanded( - flex: 3, child: Text( downloadsText, - textAlign: TextAlign.right, ), ), ], From 72ca4df5c13c5a943cf3b2b14461030ea87f3e72 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 3 Aug 2026 13:34:42 -0700 Subject: [PATCH 03/17] Update main.dart --- pkgs/flutter_http_example/lib/main.dart | 297 +++++++++++++++++------- 1 file changed, 212 insertions(+), 85 deletions(-) diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 30d2924c9f..ac8da8bb86 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -23,10 +23,38 @@ class PackageSearchApp extends StatelessWidget { const PackageSearchApp({super.key}); @override - Widget build(BuildContext context) => const MaterialApp( + Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, title: 'Pub.dev Package Search', - home: HomePage(), + theme: ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0F172A), + colorScheme: const ColorScheme.dark( + primary: Color(0xFF38BDF8), + surface: Color(0xFF1E293B), + onSurface: Colors.white, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFF1E293B), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFF334155)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFF334155)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFF38BDF8), width: 2), + ), + prefixIconColor: const Color(0xFF94A3B8), + labelStyle: const TextStyle(color: Color(0xFF94A3B8)), + ), + ), + home: const HomePage(), ); } @@ -58,9 +86,10 @@ class _HomePageState extends State { 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() + 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; @@ -95,9 +124,26 @@ class _HomePageState extends State { Widget build(BuildContext context) { Widget body; if (_loadingPackages) { - body = const Center(child: CircularProgressIndicator()); + body = const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text( + 'Loading package database...', + style: TextStyle(color: Color(0xFF94A3B8)), + ), + ], + ), + ); } else if (_matchingNames == null) { - body = const Center(child: Text('Loading...')); + body = const Center( + child: Text( + 'Loading...', + style: TextStyle(color: Color(0xFF94A3B8)), + ), + ); } else { body = PackageTable( packageNames: _matchingNames!, @@ -106,18 +152,30 @@ class _HomePageState extends State { } return Scaffold( - appBar: AppBar(title: const Text('Pub.dev Package Search')), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( + appBar: AppBar( + title: const Text( + 'Pub.dev Package Search', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white), + ), + backgroundColor: const Color(0xFF0F172A), + centerTitle: true, + elevation: 0, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Column( + children: [ + TextField( onChanged: _runSearch, - decoration: const InputDecoration(labelText: 'Search packages'), + decoration: const InputDecoration( + labelText: 'Search packages', + prefixIcon: Icon(Icons.search), + ), ), - ), - Expanded(child: body), - ], + const SizedBox(height: 20), + Expanded(child: body), + ], + ), ), ); } @@ -134,55 +192,93 @@ class PackageTable extends StatelessWidget { }); @override - Widget build(BuildContext context) => Column( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: Text( - 'Package Name', + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFF334155)), + borderRadius: BorderRadius.circular(16), + color: theme.colorScheme.surface, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Column( + children: [ + Container( + color: const Color(0xFF1E293B), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + child: const Row( + children: [ + Expanded( + flex: 4, + child: Text( + 'Package Name', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xFF38BDF8), + fontSize: 15, + ), + ), ), - ), - Expanded( - child: Text( - 'Likes', + Expanded( + flex: 2, + child: Text( + 'Likes', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xFF38BDF8), + fontSize: 15, + ), + textAlign: TextAlign.right, + ), ), - ), - Expanded( - child: Text( - '30-day Downloads', + Expanded( + flex: 3, + child: Text( + '30-day Downloads', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xFF38BDF8), + fontSize: 15, + ), + textAlign: TextAlign.right, + ), ), - ), - ], + ], + ), ), - ), - const Divider(height: 1), - Expanded( - child: ListView.builder( - itemCount: packageNames.length, - itemBuilder: (context, index) { - final name = packageNames[index]; - return PackageRow( - key: ValueKey(name), - name: name, - client: client, - ); - }, + const Divider(height: 1, color: Color(0xFF334155)), + Expanded( + child: ListView.builder( + itemCount: packageNames.length, + itemBuilder: (context, index) { + final name = packageNames[index]; + final isEven = index.isEven; + return PackageRow( + key: ValueKey(name), + name: name, + client: client, + isEven: isEven, + ); + }, + ), ), - ), - ], - ); + ], + ), + ), + ); + } } class PackageRow extends StatefulWidget { final String name; final Client client; + final bool isEven; const PackageRow({ required this.name, required this.client, + required this.isEven, super.key, }); @@ -204,8 +300,8 @@ class _PackageRowState extends State { Uri.https('pub.dev', '/api/packages/$name/score'), ); if (response.statusCode == 200) { - final json = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; + final json = jsonDecode(utf8.decode(response.bodyBytes)) + as Map; return Package.fromJson(name, json); } else { throw Exception('Failed to load score'); @@ -213,41 +309,72 @@ class _PackageRowState extends State { } @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: Text(widget.name), + Widget build(BuildContext context) { + final rowBgColor = + widget.isEven ? const Color(0xFF0F172A) : const Color(0xFF1E293B); + + return Container( + color: rowBgColor, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), + child: Row( + children: [ + Expanded( + flex: 4, + child: Text( + widget.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white, + ), ), - Expanded( - child: FutureBuilder( - future: _scoreFuture, - builder: (context, snapshot) { - final package = snapshot.data; - final likesText = - package != null ? package.likes.toString() : '...'; - final downloadsText = - package != null ? package.downloads.toString() : '...'; - - return Row( - children: [ - Expanded( - child: Text( - likesText, + ), + Expanded( + flex: 5, + child: FutureBuilder( + future: _scoreFuture, + builder: (context, snapshot) { + final package = snapshot.data; + final likesText = + package != null ? package.likes.toString() : '...'; + final downloadsText = + package != null ? package.downloads.toString() : '...'; + + final isWaiting = + snapshot.connectionState == ConnectionState.waiting; + + return Row( + children: [ + Expanded( + flex: 2, + child: Text( + likesText, + style: TextStyle( + color: isWaiting + ? const Color(0xFF64748B) + : const Color(0xFFE2E8F0), ), + textAlign: TextAlign.right, ), - Expanded( - child: Text( - downloadsText, + ), + Expanded( + flex: 3, + child: Text( + downloadsText, + style: TextStyle( + color: isWaiting + ? const Color(0xFF64748B) + : const Color(0xFFE2E8F0), ), + textAlign: TextAlign.right, ), - ], - ); - }, - ), + ), + ], + ); + }, ), - ], - ), - ); + ), + ], + ), + ); + } } From 2cf7653a63f72595932a6362223019e838a5976b Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 3 Aug 2026 16:45:14 -0700 Subject: [PATCH 04/17] work --- pkgs/flutter_http_example/lib/main.dart | 316 +++++++++--------- pkgs/flutter_http_example/lib/package.dart | 10 +- .../test/widget_test.dart | 110 +++++- 3 files changed, 270 insertions(+), 166 deletions(-) diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index ac8da8bb86..6f82ccc067 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http_image_provider/http_image_provider.dart'; import 'package:provider/provider.dart'; import 'http_client_factory.dart' @@ -86,10 +87,9 @@ class _HomePageState extends State { 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() + 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; @@ -145,7 +145,7 @@ class _HomePageState extends State { ), ); } else { - body = PackageTable( + body = PackageList( packageNames: _matchingNames!, client: _client, ); @@ -181,199 +181,205 @@ class _HomePageState extends State { } } -class PackageTable extends StatelessWidget { +class PackageList extends StatelessWidget { final List packageNames; final Client client; - const PackageTable({ + const PackageList({ required this.packageNames, required this.client, super.key, }); @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - border: Border.all(color: const Color(0xFF334155)), - borderRadius: BorderRadius.circular(16), - color: theme.colorScheme.surface, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Column( - children: [ - Container( - color: const Color(0xFF1E293B), - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), - child: const Row( - children: [ - Expanded( - flex: 4, - child: Text( - 'Package Name', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Color(0xFF38BDF8), - fontSize: 15, - ), - ), - ), - Expanded( - flex: 2, - child: Text( - 'Likes', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Color(0xFF38BDF8), - fontSize: 15, - ), - textAlign: TextAlign.right, - ), - ), - Expanded( - flex: 3, - child: Text( - '30-day Downloads', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Color(0xFF38BDF8), - fontSize: 15, - ), - textAlign: TextAlign.right, - ), - ), - ], - ), - ), - const Divider(height: 1, color: Color(0xFF334155)), - Expanded( - child: ListView.builder( - itemCount: packageNames.length, - itemBuilder: (context, index) { - final name = packageNames[index]; - final isEven = index.isEven; - return PackageRow( - key: ValueKey(name), - name: name, - client: client, - isEven: isEven, - ); - }, - ), - ), - ], - ), - ), - ); - } + 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 PackageRow extends StatefulWidget { +class PackageCard extends StatefulWidget { final String name; final Client client; - final bool isEven; - const PackageRow({ + const PackageCard({ required this.name, required this.client, - required this.isEven, super.key, }); @override - State createState() => _PackageRowState(); + State createState() => _PackageCardState(); } -class _PackageRowState extends State { - late Future _scoreFuture; +class _PackageCardState extends State { + late Future _packageFuture; @override void initState() { super.initState(); - _scoreFuture = _fetchScore(widget.name, widget.client); + _packageFuture = _fetchPackageInfo(widget.name, widget.client); } - Future _fetchScore(String name, Client client) async { - final response = await client.get( - Uri.https('pub.dev', '/api/packages/$name/score'), - ); - if (response.statusCode == 200) { - final json = jsonDecode(utf8.decode(response.bodyBytes)) + 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; - return Package.fromJson(name, json); + final publisherId = publisherJson['publisherId'] as String?; + return Package.fromJson(name, scoreJson, publisherId: publisherId); } else { - throw Exception('Failed to load score'); + throw Exception('Failed to load package info'); } } @override Widget build(BuildContext context) { - final rowBgColor = - widget.isEven ? const Color(0xFF0F172A) : const Color(0xFF1E293B); - - return Container( - color: rowBgColor, - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), - child: Row( - children: [ - Expanded( - flex: 4, - child: Text( - widget.name, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - Expanded( - flex: 5, - child: FutureBuilder( - future: _scoreFuture, - builder: (context, snapshot) { - final package = snapshot.data; - final likesText = - package != null ? package.likes.toString() : '...'; - final downloadsText = - package != null ? package.downloads.toString() : '...'; + final theme = Theme.of(context); + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + elevation: 0, + color: theme.colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Color(0xFF334155)), + ), + 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, + color: Colors.white, + ), + ), + const SizedBox(height: 12), + const LinearProgressIndicator( + color: Color(0xFF38BDF8), + backgroundColor: Color(0xFF1E293B), + ), + ], + ); + } - final isWaiting = - snapshot.connectionState == ConnectionState.waiting; + if (snapshot.hasError) { + return Text( + 'Error loading info for ${widget.name}', + style: const TextStyle(color: Colors.redAccent), + ); + } - return Row( + final package = snapshot.data!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - flex: 2, - child: Text( - likesText, - style: TextStyle( - color: isWaiting - ? const Color(0xFF64748B) - : const Color(0xFFE2E8F0), - ), - textAlign: TextAlign.right, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + package.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF38BDF8), + ), + ), + if (package.publisherId != null) ...[ + const SizedBox(height: 6), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image( + image: HttpImageProvider( + Uri.parse( + '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, + color: Color(0xFF94A3B8), + ), + ), + ), + const SizedBox(width: 6), + Text( + package.publisherId!, + style: const TextStyle( + color: Color(0xFF34D399), + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ], ), ), - Expanded( - flex: 3, - child: Text( - downloadsText, - style: TextStyle( - color: isWaiting - ? const Color(0xFF64748B) - : const Color(0xFFE2E8F0), - ), - textAlign: TextAlign.right, - ), + ], + ), + const SizedBox(height: 14), + Row( + children: [ + const Icon(Icons.thumb_up_outlined, + size: 16, color: Color(0xFF94A3B8)), + const SizedBox(width: 6), + Text( + '${package.likes} Likes', + style: const TextStyle( + color: Color(0xFF94A3B8), fontSize: 14), + ), + const SizedBox(width: 24), + const Icon(Icons.download_outlined, + size: 16, color: Color(0xFF94A3B8)), + const SizedBox(width: 6), + Text( + '${package.downloads} Downloads', + style: const TextStyle( + color: Color(0xFF94A3B8), fontSize: 14), ), ], - ); - }, - ), - ), - ], + ), + ], + ); + }, + ), ), ); } diff --git a/pkgs/flutter_http_example/lib/package.dart b/pkgs/flutter_http_example/lib/package.dart index 9eaa98b590..b04d0d5320 100644 --- a/pkgs/flutter_http_example/lib/package.dart +++ b/pkgs/flutter_http_example/lib/package.dart @@ -6,16 +6,24 @@ 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) => Package( + 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/test/widget_test.dart b/pkgs/flutter_http_example/test/widget_test.dart index 83c6a2cdda..cde96d43b1 100644 --- a/pkgs/flutter_http_example/test/widget_test.dart +++ b/pkgs/flutter_http_example/test/widget_test.dart @@ -40,6 +40,76 @@ const _sharedPrefsScoreResponse = ''' } '''; +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, @@ -53,10 +123,19 @@ void main() { 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}'); }); @@ -68,31 +147,33 @@ void main() { // Verify all package names are shown. expect( find.descendant( - of: find.byType(PackageTable), + of: find.byType(PackageList), matching: find.text('http'), ), findsOneWidget, ); expect( find.descendant( - of: find.byType(PackageTable), + of: find.byType(PackageList), matching: find.text('http_parser'), ), findsOneWidget, ); expect( find.descendant( - of: find.byType(PackageTable), + 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'), findsOneWidget); - expect(find.text('9436929'), findsOneWidget); - expect(find.text('3200'), findsOneWidget); - expect(find.text('4000000'), findsOneWidget); + expect(find.text('8458 Likes'), findsOneWidget); + expect(find.text('9436929 Downloads'), findsOneWidget); }); testWidgets('test search filters results', (WidgetTester tester) async { @@ -101,10 +182,19 @@ void main() { 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); }); @@ -120,14 +210,14 @@ void main() { // The matching package names should be present. expect( find.descendant( - of: find.byType(PackageTable), + of: find.byType(PackageList), matching: find.text('http'), ), findsOneWidget, ); expect( find.descendant( - of: find.byType(PackageTable), + of: find.byType(PackageList), matching: find.text('http_parser'), ), findsOneWidget, @@ -136,7 +226,7 @@ void main() { // The non-matching package should not be present. expect( find.descendant( - of: find.byType(PackageTable), + of: find.byType(PackageList), matching: find.text('shared_preferences'), ), findsNothing, From c94287606c2f3d8edce1337c3f576188750e713c Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 3 Aug 2026 16:48:01 -0700 Subject: [PATCH 05/17] Update main.dart --- pkgs/flutter_http_example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 6f82ccc067..dced63e1ac 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -26,7 +26,7 @@ class PackageSearchApp extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, - title: 'Pub.dev Package Search', + title: 'Dart Package Search', theme: ThemeData( useMaterial3: true, brightness: Brightness.dark, From 24a8246f90d03e5a0170ab3ecdeab64e100df3c6 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 3 Aug 2026 16:55:31 -0700 Subject: [PATCH 06/17] Update main.dart --- pkgs/flutter_http_example/lib/main.dart | 257 ++++++++---------------- 1 file changed, 87 insertions(+), 170 deletions(-) diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index dced63e1ac..45282d8b30 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -27,46 +27,20 @@ class PackageSearchApp extends StatelessWidget { Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, title: 'Dart Package Search', - theme: ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - scaffoldBackgroundColor: const Color(0xFF0F172A), - colorScheme: const ColorScheme.dark( - primary: Color(0xFF38BDF8), - surface: Color(0xFF1E293B), - onSurface: Colors.white, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: const Color(0xFF1E293B), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide(color: Color(0xFF334155)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide(color: Color(0xFF334155)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide(color: Color(0xFF38BDF8), width: 2), - ), - prefixIconColor: const Color(0xFF94A3B8), - labelStyle: const TextStyle(color: Color(0xFF94A3B8)), - ), - ), - home: const HomePage(), + 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 { +class _SearchPageState extends State { List? _allPackages; List? _matchingNames; bool _loadingPackages = false; @@ -130,20 +104,12 @@ class _HomePageState extends State { children: [ CircularProgressIndicator(), SizedBox(height: 16), - Text( - 'Loading package database...', - style: TextStyle(color: Color(0xFF94A3B8)), - ), + Text('Loading package database...'), ], ), ); } else if (_matchingNames == null) { - body = const Center( - child: Text( - 'Loading...', - style: TextStyle(color: Color(0xFF94A3B8)), - ), - ); + body = const Center(child: Text('Loading...')); } else { body = PackageList( packageNames: _matchingNames!, @@ -153,16 +119,11 @@ class _HomePageState extends State { return Scaffold( appBar: AppBar( - title: const Text( - 'Pub.dev Package Search', - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white), - ), - backgroundColor: const Color(0xFF0F172A), + title: const Text('Dart Package Search'), centerTitle: true, - elevation: 0, ), body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: const EdgeInsets.all(16), child: Column( children: [ TextField( @@ -170,9 +131,10 @@ class _HomePageState extends State { decoration: const InputDecoration( labelText: 'Search packages', prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), ), ), - const SizedBox(height: 20), + const SizedBox(height: 16), Expanded(child: body), ], ), @@ -251,136 +213,91 @@ class _PackageCardState extends State { } @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Card( - margin: const EdgeInsets.symmetric(vertical: 8), - elevation: 0, - color: theme.colorScheme.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide(color: Color(0xFF334155)), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: FutureBuilder( - future: _packageFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { + 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( - widget.name, + package.name, style: const TextStyle( - fontSize: 16, + fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.white, ), ), - const SizedBox(height: 12), - const LinearProgressIndicator( - color: Color(0xFF38BDF8), - backgroundColor: Color(0xFF1E293B), - ), - ], - ); - } - - if (snapshot.hasError) { - return Text( - 'Error loading info for ${widget.name}', - style: const TextStyle(color: Colors.redAccent), - ); - } - - final package = snapshot.data!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - package.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF38BDF8), + if (package.publisherId != null) ...[ + const SizedBox(height: 6), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image( + image: HttpImageProvider( + Uri.parse( + '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), ), - if (package.publisherId != null) ...[ - const SizedBox(height: 6), - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image( - image: HttpImageProvider( - Uri.parse( - '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, - color: Color(0xFF94A3B8), - ), - ), - ), - const SizedBox(width: 6), - Text( - package.publisherId!, - style: const TextStyle( - color: Color(0xFF34D399), - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - ], - ), - ), - ], - ), - const SizedBox(height: 14), - Row( - children: [ - const Icon(Icons.thumb_up_outlined, - size: 16, color: Color(0xFF94A3B8)), - const SizedBox(width: 6), - Text( - '${package.likes} Likes', - style: const TextStyle( - color: Color(0xFF94A3B8), fontSize: 14), - ), - const SizedBox(width: 24), - const Icon(Icons.download_outlined, - size: 16, color: Color(0xFF94A3B8)), - const SizedBox(width: 6), - Text( - '${package.downloads} Downloads', - style: const TextStyle( - color: Color(0xFF94A3B8), fontSize: 14), + ), + 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'), + ], + ), + ], + ); + }, + ), ), - ), - ); - } + ); } From 0cfb4cab40230e0bf6d30ca7a86b0f5d9e2af58a Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 3 Aug 2026 16:57:24 -0700 Subject: [PATCH 07/17] Update main.dart --- pkgs/flutter_http_example/lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 45282d8b30..57bdc10bff 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -259,6 +259,8 @@ class _PackageCardState extends State { 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.parse( 'https://www.google.com/s2/favicons?sz=64&domain=${package.publisherId}'), From be80fcfbceee62e2e188beed1e35375185f65d57 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 3 Aug 2026 17:02:36 -0700 Subject: [PATCH 08/17] update --- .../macos/Runner.xcodeproj/project.pbxproj | 18 ------------------ pkgs/flutter_http_example/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) 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/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: From 762abe4f7e03c9b300d0285896b931a5074e55df Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 4 Aug 2026 09:16:58 -0700 Subject: [PATCH 09/17] Update main.dart --- pkgs/flutter_http_example/lib/main.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 57bdc10bff..4999360641 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -262,8 +262,10 @@ class _PackageCardState extends State { // Image.network does not allow you to provide your // own `http.Client` so use `HttpImageProvider`. image: HttpImageProvider( - Uri.parse( - 'https://www.google.com/s2/favicons?sz=64&domain=${package.publisherId}'), + Uri.https('www.google.com', '/s2/favicons', { + 'sz': '64', + 'domain': package.publisherId, + }), client: widget.client, ), width: 16, From eec91cc330484d4c741c6b431743fcbc5ba65794 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 4 Aug 2026 10:28:10 -0700 Subject: [PATCH 10/17] fixes --- .../example/android/gradle.properties | 4 + pkgs/cronet_http/example/lib/book.dart | 32 --- pkgs/cronet_http/example/lib/main.dart | 238 ++++++++++-------- pkgs/cupertino_http/example/example.dart | 24 +- pkgs/flutter_http_example/lib/main.dart | 37 ++- .../test/widget_test.dart | 51 ++++ pkgs/http/example/main.dart | 18 +- pkgs/ok_http/example/lib/book.dart | 32 --- pkgs/ok_http/example/lib/main.dart | 236 ++++++++++------- 9 files changed, 393 insertions(+), 279 deletions(-) delete mode 100644 pkgs/cronet_http/example/lib/book.dart delete mode 100644 pkgs/ok_http/example/lib/book.dart 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..57d6e0da7a 100644 --- a/pkgs/cronet_http/example/lib/main.dart +++ b/pkgs/cronet_http/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// 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. @@ -12,8 +12,6 @@ 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,134 +19,178 @@ void main() { final engine = CronetEngine.build( cacheMode: CacheMode.memory, cacheMaxSize: 2 * 1024 * 1024, - userAgent: 'Book Agent'); + userAgent: 'Package details Agent'); httpClient = CronetClient.fromCronetEngine(engine, closeEngine: true); } else { - httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); + httpClient = IOClient(HttpClient()..userAgent = 'Package details Agent'); } 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. + Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, - title: 'Book Search', - home: HomePage(), + title: 'Package Details', + theme: ThemeData(useMaterial3: true), + darkTheme: ThemeData.dark(useMaterial3: true), + home: const 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; +class _PackageDetailsPageState extends State { + late Future _packageInfoFuture; late Client _client; @override void initState() { super.initState(); _client = context.read(); + _packageInfoFuture = _fetchPackageInfo(); } - // 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); - } - - void _runSearch(String query) async { - _lastQuery = query; - if (query.isEmpty) { - setState(() { - _books = null; - }); - return; + Future _fetchPackageInfo() async { + const packageName = 'cronet_http'; + final results = await Future.wait([ + _client.get(Uri.https('pub.dev', '/api/packages/$packageName/score')), + _client.get(Uri.https('pub.dev', '/api/packages/$packageName/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; + return PackageInfo( + name: packageName, + likes: scoreJson['likeCount'] as int? ?? 0, + downloads: scoreJson['downloadCount30Days'] as int? ?? 0, + publisherId: publisherJson['publisherId'] as String?, + ); + } else { + throw Exception('Failed to load package info'); } - - 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), - ), - ), - const SizedBox(height: 20), - Expanded(child: searchResult), - ], + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Package Details'), + centerTitle: true, ), - ), - ); - } -} - -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), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: FutureBuilder( + future: _packageInfoFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } + + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + + final info = snapshot.data!; + return Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + info.name, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + if (info.publisherId != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image( + image: HttpImageProvider( + Uri.https( + 'www.google.com', '/s2/favicons', { + 'sz': '64', + 'domain': info.publisherId!, + }), + client: _client, + ), + width: 16, + height: 16, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.public, size: 16), + ), + ), + const SizedBox(width: 8), + Text( + info.publisherId!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + const SizedBox(height: 24), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.thumb_up_outlined, size: 18), + const SizedBox(width: 6), + Text('${info.likes} Likes'), + const SizedBox(width: 24), + const Icon(Icons.download_outlined, size: 18), + const SizedBox(width: 6), + Text('${info.downloads} Downloads'), + ], + ), + ], + ), + ), + ); + }, + ), ), ), ); } + +class PackageInfo { + final String name; + final int likes; + final int downloads; + final String? publisherId; + + PackageInfo({ + required this.name, + required this.likes, + required this.downloads, + required this.publisherId, + }); +} 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/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 4999360641..1fd5270ac8 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -44,6 +44,7 @@ class _SearchPageState extends State { List? _allPackages; List? _matchingNames; bool _loadingPackages = false; + String? _errorMessage; String? _lastQuery; late Client _client; @@ -55,7 +56,10 @@ class _SearchPageState extends State { } Future _loadPackageList() async { - setState(() => _loadingPackages = true); + setState(() { + _loadingPackages = true; + _errorMessage = null; + }); try { final response = await _client.get( Uri.https('pub.dev', '/api/package-name-completion-data'), @@ -70,9 +74,18 @@ class _SearchPageState extends State { _loadingPackages = false; _runSearch(_lastQuery ?? ''); }); + } else { + setState(() { + _errorMessage = + 'Failed to load package list: Status ${response.statusCode}'; + _loadingPackages = false; + }); } - } catch (_) { - setState(() => _loadingPackages = false); + } catch (e) { + setState(() { + _errorMessage = 'Failed to load package list: $e'; + _loadingPackages = false; + }); } } @@ -108,6 +121,24 @@ class _SearchPageState extends State { ], ), ); + } 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 { diff --git a/pkgs/flutter_http_example/test/widget_test.dart b/pkgs/flutter_http_example/test/widget_test.dart index cde96d43b1..d7adbc9241 100644 --- a/pkgs/flutter_http_example/test/widget_test.dart +++ b/pkgs/flutter_http_example/test/widget_test.dart @@ -232,4 +232,55 @@ void main() { findsNothing, ); }); + + testWidgets('test load package list error displays error and retry', + (WidgetTester tester) async { + var requestCount = 0; + final mockClient = MockClient((request) async { + 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.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(); + + // Verify package list loads successfully after retry. + expect( + find.descendant( + of: find.byType(PackageList), + matching: find.text('http'), + ), + findsOneWidget, + ); + }); } diff --git a/pkgs/http/example/main.dart b/pkgs/http/example/main.dart index 34149188ba..095f0da4d7 100644 --- a/pkgs/http/example/main.dart +++ b/pkgs/http/example/main.dart @@ -1,20 +1,26 @@ +// 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 + 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); 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}.'); } 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..e1eb2302b1 100644 --- a/pkgs/ok_http/example/lib/main.dart +++ b/pkgs/ok_http/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// 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. @@ -12,138 +12,180 @@ 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 details Agent'); } 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. + Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, - title: 'Book Search', - home: HomePage(), + title: 'Package Details', + theme: ThemeData(useMaterial3: true), + darkTheme: ThemeData.dark(useMaterial3: true), + home: const 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; +class _PackageDetailsPageState extends State { + late Future _packageInfoFuture; late Client _client; @override void initState() { super.initState(); _client = context.read(); + _packageInfoFuture = _fetchPackageInfo(); } - // 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); - } - - void _runSearch(String query) async { - _lastQuery = query; - if (query.isEmpty) { - setState(() { - _books = null; - }); - return; + Future _fetchPackageInfo() async { + const packageName = 'ok_http'; + final results = await Future.wait([ + _client.get(Uri.https('pub.dev', '/api/packages/$packageName/score')), + _client.get(Uri.https('pub.dev', '/api/packages/$packageName/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; + return PackageInfo( + name: packageName, + likes: scoreJson['likeCount'] as int? ?? 0, + downloads: scoreJson['downloadCount30Days'] as int? ?? 0, + publisherId: publisherJson['publisherId'] as String?, + ); + } else { + throw Exception('Failed to load package info'); } - - 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), - ), - ), - const SizedBox(height: 20), - Expanded(child: searchResult), - ], + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Package Details'), + centerTitle: true, ), - ), - ); - } -} - -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), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: FutureBuilder( + future: _packageInfoFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } + + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + + final info = snapshot.data!; + return Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + info.name, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + if (info.publisherId != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image( + image: HttpImageProvider( + Uri.https( + 'www.google.com', '/s2/favicons', { + 'sz': '64', + 'domain': info.publisherId!, + }), + client: _client, + ), + width: 16, + height: 16, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.public, size: 16), + ), + ), + const SizedBox(width: 8), + Text( + info.publisherId!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + const SizedBox(height: 24), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.thumb_up_outlined, size: 18), + const SizedBox(width: 6), + Text('${info.likes} Likes'), + const SizedBox(width: 24), + const Icon(Icons.download_outlined, size: 18), + const SizedBox(width: 6), + Text('${info.downloads} Downloads'), + ], + ), + ], + ), + ), + ); + }, + ), ), ), ); } + +class PackageInfo { + final String name; + final int likes; + final int downloads; + final String? publisherId; + + PackageInfo({ + required this.name, + required this.likes, + required this.downloads, + required this.publisherId, + }); +} From 0c31dd8987ebdfb7539d5a557ec55255c5b6b01c Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 4 Aug 2026 10:31:54 -0700 Subject: [PATCH 11/17] fix --- pkgs/cronet_http/example/lib/main.dart | 158 +++++-------------------- pkgs/cronet_http/example/pubspec.yaml | 2 - pkgs/ok_http/example/lib/main.dart | 158 +++++-------------------- pkgs/ok_http/example/pubspec.yaml | 2 - 4 files changed, 60 insertions(+), 260 deletions(-) diff --git a/pkgs/cronet_http/example/lib/main.dart b/pkgs/cronet_http/example/lib/main.dart index 57d6e0da7a..e19b9b179f 100644 --- a/pkgs/cronet_http/example/lib/main.dart +++ b/pkgs/cronet_http/example/lib/main.dart @@ -9,7 +9,6 @@ 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'; void main() { @@ -35,12 +34,10 @@ class PackageDetailsApp extends StatelessWidget { const PackageDetailsApp({super.key}); @override - Widget build(BuildContext context) => MaterialApp( + Widget build(BuildContext context) => const MaterialApp( debugShowCheckedModeBanner: false, title: 'Package Details', - theme: ThemeData(useMaterial3: true), - darkTheme: ThemeData.dark(useMaterial3: true), - home: const PackageDetailsPage(), + home: PackageDetailsPage(), ); } @@ -52,145 +49,50 @@ class PackageDetailsPage extends StatefulWidget { } class _PackageDetailsPageState extends State { - late Future _packageInfoFuture; - late Client _client; + String _output = 'Loading...'; @override void initState() { super.initState(); - _client = context.read(); - _packageInfoFuture = _fetchPackageInfo(); + _fetchPackageInfo(); } - Future _fetchPackageInfo() async { - const packageName = 'cronet_http'; - final results = await Future.wait([ - _client.get(Uri.https('pub.dev', '/api/packages/$packageName/score')), - _client.get(Uri.https('pub.dev', '/api/packages/$packageName/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; - return PackageInfo( - name: packageName, - likes: scoreJson['likeCount'] as int? ?? 0, - downloads: scoreJson['downloadCount30Days'] as int? ?? 0, - publisherId: publisherJson['publisherId'] as String?, + void _fetchPackageInfo() async { + final client = context.read(); + try { + final response = await client.get( + Uri.https('pub.dev', '/api/packages/cronet_http/score'), ); - } else { - throw Exception('Failed to load package info'); + 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(() { + _output = 'Request failed: $e'; + }); } } @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Package Details'), - centerTitle: true, - ), - body: Center( + body: SafeArea( child: Padding( padding: const EdgeInsets.all(16), - child: FutureBuilder( - future: _packageInfoFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } - - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } - - final info = snapshot.data!; - return Card( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - info.name, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - if (info.publisherId != null) ...[ - const SizedBox(height: 8), - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image( - image: HttpImageProvider( - Uri.https( - 'www.google.com', '/s2/favicons', { - 'sz': '64', - 'domain': info.publisherId!, - }), - client: _client, - ), - width: 16, - height: 16, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.public, size: 16), - ), - ), - const SizedBox(width: 8), - Text( - info.publisherId!, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - const SizedBox(height: 24), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.thumb_up_outlined, size: 18), - const SizedBox(width: 6), - Text('${info.likes} Likes'), - const SizedBox(width: 24), - const Icon(Icons.download_outlined, size: 18), - const SizedBox(width: 6), - Text('${info.downloads} Downloads'), - ], - ), - ], - ), - ), - ); - }, + child: Text( + _output, + style: const TextStyle(fontSize: 18, fontFamily: 'monospace'), ), ), ), ); } - -class PackageInfo { - final String name; - final int likes; - final int downloads; - final String? publisherId; - - PackageInfo({ - required this.name, - required this.likes, - required this.downloads, - required this.publisherId, - }); -} 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/ok_http/example/lib/main.dart b/pkgs/ok_http/example/lib/main.dart index e1eb2302b1..ecc0345ed4 100644 --- a/pkgs/ok_http/example/lib/main.dart +++ b/pkgs/ok_http/example/lib/main.dart @@ -8,7 +8,6 @@ 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'; @@ -30,12 +29,10 @@ class PackageDetailsApp extends StatelessWidget { const PackageDetailsApp({super.key}); @override - Widget build(BuildContext context) => MaterialApp( + Widget build(BuildContext context) => const MaterialApp( debugShowCheckedModeBanner: false, title: 'Package Details', - theme: ThemeData(useMaterial3: true), - darkTheme: ThemeData.dark(useMaterial3: true), - home: const PackageDetailsPage(), + home: PackageDetailsPage(), ); } @@ -47,145 +44,50 @@ class PackageDetailsPage extends StatefulWidget { } class _PackageDetailsPageState extends State { - late Future _packageInfoFuture; - late Client _client; + String _output = 'Loading...'; @override void initState() { super.initState(); - _client = context.read(); - _packageInfoFuture = _fetchPackageInfo(); + _fetchPackageInfo(); } - Future _fetchPackageInfo() async { - const packageName = 'ok_http'; - final results = await Future.wait([ - _client.get(Uri.https('pub.dev', '/api/packages/$packageName/score')), - _client.get(Uri.https('pub.dev', '/api/packages/$packageName/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; - return PackageInfo( - name: packageName, - likes: scoreJson['likeCount'] as int? ?? 0, - downloads: scoreJson['downloadCount30Days'] as int? ?? 0, - publisherId: publisherJson['publisherId'] as String?, + void _fetchPackageInfo() async { + final client = context.read(); + try { + final response = await client.get( + Uri.https('pub.dev', '/api/packages/ok_http/score'), ); - } else { - throw Exception('Failed to load package info'); + 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(() { + _output = 'Request failed: $e'; + }); } } @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Package Details'), - centerTitle: true, - ), - body: Center( + body: SafeArea( child: Padding( padding: const EdgeInsets.all(16), - child: FutureBuilder( - future: _packageInfoFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } - - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } - - final info = snapshot.data!; - return Card( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - info.name, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - if (info.publisherId != null) ...[ - const SizedBox(height: 8), - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image( - image: HttpImageProvider( - Uri.https( - 'www.google.com', '/s2/favicons', { - 'sz': '64', - 'domain': info.publisherId!, - }), - client: _client, - ), - width: 16, - height: 16, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.public, size: 16), - ), - ), - const SizedBox(width: 8), - Text( - info.publisherId!, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - const SizedBox(height: 24), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.thumb_up_outlined, size: 18), - const SizedBox(width: 6), - Text('${info.likes} Likes'), - const SizedBox(width: 24), - const Icon(Icons.download_outlined, size: 18), - const SizedBox(width: 6), - Text('${info.downloads} Downloads'), - ], - ), - ], - ), - ), - ); - }, + child: Text( + _output, + style: const TextStyle(fontSize: 18, fontFamily: 'monospace'), ), ), ), ); } - -class PackageInfo { - final String name; - final int likes; - final int downloads; - final String? publisherId; - - PackageInfo({ - required this.name, - required this.likes, - required this.downloads, - required this.publisherId, - }); -} 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 From 1755364618d6528f283ca23c4e58a2ae5eb5ee31 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 4 Aug 2026 10:37:40 -0700 Subject: [PATCH 12/17] fix --- pkgs/cronet_http/CHANGELOG.md | 4 ++++ pkgs/cronet_http/pubspec.yaml | 2 +- pkgs/cupertino_http/CHANGELOG.md | 4 ++++ pkgs/cupertino_http/pubspec.yaml | 2 +- pkgs/flutter_http_example/lib/main.dart | 8 ++++++++ pkgs/http/CHANGELOG.md | 1 + pkgs/ok_http/CHANGELOG.md | 1 + 7 files changed, 20 insertions(+), 2 deletions(-) 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/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/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/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 1fd5270ac8..aa608c3419 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -15,6 +15,14 @@ import 'package.dart'; void main() { runApp(Provider( + // `Provider` calls its `create` argument once when a `Client` is + // first requested (through `BuildContext.read()`) and uses that + // same instance for all future requests. + // + // Reusing the same `Client` may: + // - reduce memory usage + // - allow caching of fetched URLs + // - allow connections to be persisted create: (_) => http_factory.httpClient(), child: const PackageSearchApp(), dispose: (_, client) => client.close())); 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/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 From 5bf66fb71fb29b18933b2fc509b222232e9f7dbb Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 4 Aug 2026 10:43:37 -0700 Subject: [PATCH 13/17] Fix android --- .../flutter_http_example/android/build.gradle | 13 ------------- .../android/gradle.properties | 4 ++++ .../android/settings.gradle | 19 ++++++++++++------- 3 files changed, 16 insertions(+), 20 deletions(-) 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" From 41541a7a20ef2f9b135d822d6b0419e6accbb737 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 4 Aug 2026 10:49:40 -0700 Subject: [PATCH 14/17] Update main.dart --- pkgs/cronet_http/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/cronet_http/example/lib/main.dart b/pkgs/cronet_http/example/lib/main.dart index e19b9b179f..283c2175e1 100644 --- a/pkgs/cronet_http/example/lib/main.dart +++ b/pkgs/cronet_http/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// 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. From 4aa24a155bf172e7a525a7c0140cb4e57516e674 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 6 Aug 2026 10:11:02 -0700 Subject: [PATCH 15/17] Update main.dart --- pkgs/ok_http/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/ok_http/example/lib/main.dart b/pkgs/ok_http/example/lib/main.dart index ecc0345ed4..6220c1430d 100644 --- a/pkgs/ok_http/example/lib/main.dart +++ b/pkgs/ok_http/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// 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. From f934af716aa7b50b7b0d538b0e1e29f70fdaf75a Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 6 Aug 2026 10:20:16 -0700 Subject: [PATCH 16/17] Update main.dart --- pkgs/http/example/main.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkgs/http/example/main.dart b/pkgs/http/example/main.dart index 095f0da4d7..a360c3a321 100644 --- a/pkgs/http/example/main.dart +++ b/pkgs/http/example/main.dart @@ -9,10 +9,11 @@ import 'package:http/http.dart' as http; void main(List arguments) async { // 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; @@ -24,4 +25,5 @@ void main(List arguments) async { } else { print('Request failed with status: ${response.statusCode}.'); } + client.close(); } From 334bea991f258d3d6fd2f47fb875bae77402a126 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 6 Aug 2026 10:43:23 -0700 Subject: [PATCH 17/17] Fix other doc examples --- pkgs/cronet_http/README.md | 12 ++++-------- pkgs/cronet_http/example/lib/main.dart | 4 ++-- pkgs/cronet_http/lib/cronet_http.dart | 7 ++++--- pkgs/cronet_http/lib/src/cronet_client.dart | 4 ++-- pkgs/cupertino_http/README.md | 11 +++++------ pkgs/cupertino_http/lib/cupertino_http.dart | 17 +++++++---------- .../lib/http_client_factory.dart | 6 +++--- pkgs/http/lib/src/io_client.dart | 2 +- pkgs/ok_http/example/lib/main.dart | 2 +- pkgs/ok_http/lib/ok_http.dart | 9 +++------ pkgs/ok_http/lib/src/ok_http_client.dart | 9 +++------ 11 files changed, 35 insertions(+), 48 deletions(-) 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/lib/main.dart b/pkgs/cronet_http/example/lib/main.dart index 283c2175e1..d9d171581c 100644 --- a/pkgs/cronet_http/example/lib/main.dart +++ b/pkgs/cronet_http/example/lib/main.dart @@ -18,10 +18,10 @@ void main() { final engine = CronetEngine.build( cacheMode: CacheMode.memory, cacheMaxSize: 2 * 1024 * 1024, - userAgent: 'Package details Agent'); + userAgent: 'Package Client'); httpClient = CronetClient.fromCronetEngine(engine, closeEngine: true); } else { - httpClient = IOClient(HttpClient()..userAgent = 'Package details Agent'); + httpClient = IOClient(HttpClient()..userAgent = 'Package Client'); } runApp(Provider( 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/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/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/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/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/example/lib/main.dart b/pkgs/ok_http/example/lib/main.dart index 6220c1430d..36d35b0fa3 100644 --- a/pkgs/ok_http/example/lib/main.dart +++ b/pkgs/ok_http/example/lib/main.dart @@ -16,7 +16,7 @@ void main() { if (Platform.isAndroid) { httpClient = OkHttpClient(); } else { - httpClient = IOClient(HttpClient()..userAgent = 'Package details Agent'); + httpClient = IOClient(HttpClient()..userAgent = 'Package Client'); } runApp(Provider( 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 {