From e890f6befec8f205093f2c26b6a054f040d6896a Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Fri, 3 Nov 2023 13:16:36 -0700 Subject: [PATCH 1/2] =?UTF-8?q?[go=5Frouter]=20Adds=20an=20ability=20to=20?= =?UTF-8?q?add=20a=20custom=20codec=20for=20serializing/des=E2=80=A6=20(#5?= =?UTF-8?q?288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …erializing extra fixes https://github.com/flutter/flutter/issues/99099 fixes https://github.com/flutter/flutter/issues/137248 --- packages/go_router/CHANGELOG.md | 4 + packages/go_router/doc/navigation.md | 20 +++ packages/go_router/example/README.md | 5 + .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../go_router/example/lib/extra_codec.dart | 139 ++++++++++++++++++ .../example/test/extra_codec_test.dart | 23 +++ packages/go_router/lib/src/configuration.dart | 15 ++ packages/go_router/lib/src/logging.dart | 4 +- packages/go_router/lib/src/match.dart | 65 ++++++-- packages/go_router/lib/src/router.dart | 5 + packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/extra_codec_test.dart | 111 ++++++++++++++ packages/go_router/test/test_helpers.dart | 28 ++++ 14 files changed, 404 insertions(+), 21 deletions(-) create mode 100644 packages/go_router/example/lib/extra_codec.dart create mode 100644 packages/go_router/example/test/extra_codec_test.dart create mode 100644 packages/go_router/test/extra_codec_test.dart diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 6791a2c917e..63e9cdbf4ba 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.1.0 + +- Adds an ability to add a custom codec for serializing/deserializing extra. + ## 12.0.3 - Fixes crashes when dynamically updates routing tables with named routes. diff --git a/packages/go_router/doc/navigation.md b/packages/go_router/doc/navigation.md index f5fa16ac4ba..f351d2b821d 100644 --- a/packages/go_router/doc/navigation.md +++ b/packages/go_router/doc/navigation.md @@ -84,5 +84,25 @@ Returning a value: onTap: () => context.pop(true) ``` +## Using extra +You can provide additional data along with navigation. + +```dart +context.go('/123, extra: 'abc'); +``` + +and retrieve the data from GoRouterState + +```dart +final String extraString = GoRouterState.of(context).extra! as String; +``` + +The extra data will go through serialization when it is stored in the browser. +If you plan to use complex data as extra, consider also providing a codec +to GoRouter so that it won't get dropped during serialization. + +For an example on how to use complex data in extra with a codec, see +[extra_codec.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart). + [Named routes]: https://pub.dev/documentation/go_router/latest/topics/Named%20routes-topic.html diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 55f75bca6a6..cac540fa0f8 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -41,6 +41,11 @@ An example to demonstrate how to use a `StatefulShellRoute` to create stateful n An example to demonstrate how to handle exception in go_router. +## [Extra Codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart) +`flutter run lib/extra_codec.dart` + +An example to demonstrate how to use a complex object as extra. + ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) `flutter run lib/books/main.dart` diff --git a/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj b/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj index 0841413a1fd..8f3ef0d66bf 100644 --- a/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6e1fb..b52b2e698b7 100644 --- a/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ runApp(const MyApp()); + +/// The router configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + ], + extraCodec: const MyExtraCodec(), +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen. +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "If running in web, use the browser's backward and forward button to test extra codec after setting extra several times."), + Text( + 'The extra for this page is: ${GoRouterState.of(context).extra}'), + ElevatedButton( + onPressed: () => context.go('/', extra: ComplexData1('data')), + child: const Text('Set extra to ComplexData1'), + ), + ElevatedButton( + onPressed: () => context.go('/', extra: ComplexData2('data')), + child: const Text('Set extra to ComplexData2'), + ), + ], + ), + ), + ); + } +} + +/// A complex class. +class ComplexData1 { + /// Create a complex object. + ComplexData1(this.data); + + /// The data. + final String data; + + @override + String toString() => 'ComplexData1(data: $data)'; +} + +/// A complex class. +class ComplexData2 { + /// Create a complex object. + ComplexData2(this.data); + + /// The data. + final String data; + + @override + String toString() => 'ComplexData2(data: $data)'; +} + +/// A codec that can serialize both [ComplexData1] and [ComplexData2]. +class MyExtraCodec extends Codec { + /// Create a codec. + const MyExtraCodec(); + @override + Converter get decoder => const _MyExtraDecoder(); + + @override + Converter get encoder => const _MyExtraEncoder(); +} + +class _MyExtraDecoder extends Converter { + const _MyExtraDecoder(); + @override + Object? convert(Object? input) { + if (input == null) { + return null; + } + final List inputAsList = input as List; + if (inputAsList[0] == 'ComplexData1') { + return ComplexData1(inputAsList[1]! as String); + } + if (inputAsList[0] == 'ComplexData2') { + return ComplexData2(inputAsList[1]! as String); + } + throw FormatException('Unable tp parse input: $input'); + } +} + +class _MyExtraEncoder extends Converter { + const _MyExtraEncoder(); + @override + Object? convert(Object? input) { + if (input == null) { + return null; + } + switch (input.runtimeType) { + case ComplexData1: + return ['ComplexData1', (input as ComplexData1).data]; + case ComplexData2: + return ['ComplexData2', (input as ComplexData2).data]; + default: + throw FormatException('Cannot encode type ${input.runtimeType}'); + } + } +} diff --git a/packages/go_router/example/test/extra_codec_test.dart b/packages/go_router/example/test/extra_codec_test.dart new file mode 100644 index 00000000000..7358cfc614e --- /dev/null +++ b/packages/go_router/example/test/extra_codec_test.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/extra_codec.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('The extra for this page is: null'), findsOneWidget); + + await tester.tap(find.text('Set extra to ComplexData1')); + await tester.pumpAndSettle(); + expect(find.text('The extra for this page is: ComplexData1(data: data)'), + findsOneWidget); + + await tester.tap(find.text('Set extra to ComplexData2')); + await tester.pumpAndSettle(); + expect(find.text('The extra for this page is: ComplexData2(data: data)'), + findsOneWidget); + }); +} diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index a5ff0fda5b4..5149d18e852 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -25,6 +26,7 @@ class RouteConfiguration { RouteConfiguration( this._routingConfig, { required this.navigatorKey, + this.extraCodec, }) { _onRoutingTableChanged(); _routingConfig.addListener(_onRoutingTableChanged); @@ -232,6 +234,19 @@ class RouteConfiguration { /// The global key for top level navigator. final GlobalKey navigatorKey; + /// The codec used to encode and decode extra into a serializable format. + /// + /// When navigating using [GoRouter.go] or [GoRouter.push], one can provide + /// an `extra` parameter along with it. If the extra contains complex data, + /// consider provide a codec for serializing and deserializing the extra data. + /// + /// See also: + /// * [Navigation](https://pub.dev/documentation/go_router/latest/topics/Navigation-topic.html) + /// topic. + /// * [extra_codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart) + /// example. + final Codec? extraCodec; + final Map _nameToPath = {}; /// Looks up the url location by a [GoRoute]'s name. diff --git a/packages/go_router/lib/src/logging.dart b/packages/go_router/lib/src/logging.dart index 3f39e4dc804..7f0a8ce5a7a 100644 --- a/packages/go_router/lib/src/logging.dart +++ b/packages/go_router/lib/src/logging.dart @@ -16,9 +16,9 @@ final Logger logger = Logger('GoRouter'); bool _enabled = false; /// Logs the message if logging is enabled. -void log(String message) { +void log(String message, {Level level = Level.INFO}) { if (_enabled) { - logger.info(message); + logger.log(level, message); } } diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index e5ffdec31d7..3b7a9468869 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -8,9 +8,11 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'configuration.dart'; +import 'logging.dart'; import 'misc/errors.dart'; import 'path_utils.dart'; import 'route.dart'; @@ -358,21 +360,25 @@ class RouteMatchList { /// Handles encoding and decoding of [RouteMatchList] objects to a format /// suitable for using with [StandardMessageCodec]. /// -/// The primary use of this class is for state restoration. +/// The primary use of this class is for state restoration and browser history. @internal class RouteMatchListCodec extends Codec> { /// Creates a new [RouteMatchListCodec] object. RouteMatchListCodec(RouteConfiguration configuration) - : decoder = _RouteMatchListDecoder(configuration); + : decoder = _RouteMatchListDecoder(configuration), + encoder = _RouteMatchListEncoder(configuration); static const String _locationKey = 'location'; static const String _extraKey = 'state'; static const String _imperativeMatchesKey = 'imperativeMatches'; static const String _pageKey = 'pageKey'; + static const String _codecKey = 'codec'; + static const String _jsonCodecName = 'json'; + static const String _customCodecName = 'custom'; + static const String _encodedKey = 'encoded'; @override - final Converter> encoder = - const _RouteMatchListEncoder(); + final Converter> encoder; @override final Converter, RouteMatchList> decoder; @@ -380,7 +386,9 @@ class RouteMatchListCodec extends Codec> { class _RouteMatchListEncoder extends Converter> { - const _RouteMatchListEncoder(); + const _RouteMatchListEncoder(this.configuration); + + final RouteConfiguration configuration; @override Map convert(RouteMatchList input) { final List> imperativeMatches = input.matches @@ -394,15 +402,36 @@ class _RouteMatchListEncoder imperativeMatches: imperativeMatches); } - static Map _toPrimitives(String location, Object? extra, + Map _toPrimitives(String location, Object? extra, {List>? imperativeMatches, String? pageKey}) { - String? encodedExtra; - try { - encodedExtra = json.encoder.convert(extra); - } on JsonUnsupportedObjectError {/* give up if not serializable */} + Map encodedExtra; + if (configuration.extraCodec != null) { + encodedExtra = { + RouteMatchListCodec._codecKey: RouteMatchListCodec._customCodecName, + RouteMatchListCodec._encodedKey: + configuration.extraCodec?.encode(extra), + }; + } else { + String jsonEncodedExtra; + try { + jsonEncodedExtra = json.encoder.convert(extra); + } on JsonUnsupportedObjectError { + jsonEncodedExtra = json.encoder.convert(null); + log( + 'An extra with complex data type ${extra.runtimeType} is provided ' + 'without a codec. Consider provide a codec to GoRouter to ' + 'prevent extra being dropped during serialization.', + level: Level.WARNING); + } + encodedExtra = { + RouteMatchListCodec._codecKey: RouteMatchListCodec._jsonCodecName, + RouteMatchListCodec._encodedKey: jsonEncodedExtra, + }; + } + return { RouteMatchListCodec._locationKey: location, - if (encodedExtra != null) RouteMatchListCodec._extraKey: encodedExtra, + RouteMatchListCodec._extraKey: encodedExtra, if (imperativeMatches != null) RouteMatchListCodec._imperativeMatchesKey: imperativeMatches, if (pageKey != null) RouteMatchListCodec._pageKey: pageKey, @@ -420,13 +449,17 @@ class _RouteMatchListDecoder RouteMatchList convert(Map input) { final String rootLocation = input[RouteMatchListCodec._locationKey]! as String; - final String? encodedExtra = - input[RouteMatchListCodec._extraKey] as String?; + final Map encodedExtra = + input[RouteMatchListCodec._extraKey]! as Map; final Object? extra; - if (encodedExtra != null) { - extra = json.decoder.convert(encodedExtra); + + if (encodedExtra[RouteMatchListCodec._codecKey] == + RouteMatchListCodec._jsonCodecName) { + extra = json.decoder + .convert(encodedExtra[RouteMatchListCodec._encodedKey]! as String); } else { - extra = null; + extra = configuration.extraCodec + ?.decode(encodedExtra[RouteMatchListCodec._encodedKey]); } RouteMatchList matchList = configuration.findMatch(rootLocation, extra: extra); diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index f7f8e2b0f3a..0f27dcf48c4 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -121,6 +122,7 @@ class GoRouter implements RouterConfig { /// The `routes` must not be null and must contain an [GoRouter] to match `/`. factory GoRouter({ required List routes, + Codec? extraCodec, GoExceptionHandler? onException, GoRouterPageBuilder? errorPageBuilder, GoRouterWidgetBuilder? errorBuilder, @@ -144,6 +146,7 @@ class GoRouter implements RouterConfig { redirect: redirect ?? RoutingConfig._defaultRedirect, redirectLimit: redirectLimit), ), + extraCodec: extraCodec, onException: onException, errorPageBuilder: errorPageBuilder, errorBuilder: errorBuilder, @@ -165,6 +168,7 @@ class GoRouter implements RouterConfig { /// See [routing_config.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/routing_config.dart). GoRouter.routingConfig({ required ValueListenable routingConfig, + Codec? extraCodec, GoExceptionHandler? onException, GoRouterPageBuilder? errorPageBuilder, GoRouterWidgetBuilder? errorBuilder, @@ -201,6 +205,7 @@ class GoRouter implements RouterConfig { configuration = RouteConfiguration( _routingConfig, navigatorKey: navigatorKey, + extraCodec: extraCodec, ); final ParserExceptionHandler? parserExceptionHandler; diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index e2c5cb5aa0b..f37ba325b30 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 12.0.3 +version: 12.1.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/extra_codec_test.dart b/packages/go_router/test/extra_codec_test.dart new file mode 100644 index 00000000000..3c858cad17d --- /dev/null +++ b/packages/go_router/test/extra_codec_test.dart @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. 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_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +import 'test_helpers.dart'; + +void main() { + testWidgets('router rebuild with extra codec works', + (WidgetTester tester) async { + const String initialString = 'some string'; + const String empty = 'empty'; + final GoRouter router = GoRouter( + initialLocation: '/', + extraCodec: ComplexDataCodec(), + initialExtra: ComplexData(initialString), + routes: [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) { + return Text((state.extra as ComplexData?)?.data ?? empty); + }), + ], + redirect: (BuildContext context, _) { + // Set up dependency. + SimpleDependencyProvider.of(context); + return null; + }, + ); + final SimpleDependency dependency = SimpleDependency(); + addTearDown(() => dependency.dispose()); + + await tester.pumpWidget( + SimpleDependencyProvider( + dependency: dependency, + child: MaterialApp.router( + routerConfig: router, + ), + ), + ); + expect(find.text(initialString), findsOneWidget); + dependency.boolProperty = !dependency.boolProperty; + + await tester.pumpAndSettle(); + expect(find.text(initialString), findsOneWidget); + }); + + testWidgets('Restores state correctly', (WidgetTester tester) async { + const String initialString = 'some string'; + const String empty = 'empty'; + final List routes = [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) { + return Text((state.extra as ComplexData?)?.data ?? empty); + }, + ), + ]; + + await createRouter( + routes, + tester, + initialExtra: ComplexData(initialString), + restorationScopeId: 'test', + extraCodec: ComplexDataCodec(), + ); + expect(find.text(initialString), findsOneWidget); + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text(initialString), findsOneWidget); + }); +} + +class ComplexData { + ComplexData(this.data); + final String data; +} + +class ComplexDataCodec extends Codec { + @override + Converter get decoder => ComplexDataDecoder(); + @override + Converter get encoder => ComplexDataEncoder(); +} + +class ComplexDataDecoder extends Converter { + @override + ComplexData? convert(Object? input) { + if (input == null) { + return null; + } + return ComplexData(input as String); + } +} + +class ComplexDataEncoder extends Converter { + @override + Object? convert(ComplexData? input) { + if (input == null) { + return null; + } + return input.data; + } +} diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 76ec2874a7a..3db0b457908 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -4,6 +4,8 @@ // ignore_for_file: cascade_invocations, diagnostic_describe_all_properties +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -167,6 +169,7 @@ Future createRouter( GlobalKey? navigatorKey, GoRouterWidgetBuilder? errorBuilder, String? restorationScopeId, + Codec? extraCodec, GoExceptionHandler? onException, bool requestFocus = true, bool overridePlatformDefaultLocation = false, @@ -174,6 +177,7 @@ Future createRouter( final GoRouter goRouter = GoRouter( routes: routes, redirect: redirect, + extraCodec: extraCodec, initialLocation: initialLocation, onException: onException, initialExtra: initialExtra, @@ -391,3 +395,27 @@ RouteConfiguration createRouteConfiguration({ )), navigatorKey: navigatorKey); } + +class SimpleDependencyProvider extends InheritedNotifier { + const SimpleDependencyProvider( + {super.key, required SimpleDependency dependency, required super.child}) + : super(notifier: dependency); + + static SimpleDependency of(BuildContext context) { + final SimpleDependencyProvider result = + context.dependOnInheritedWidgetOfExactType()!; + return result.notifier!; + } +} + +class SimpleDependency extends ChangeNotifier { + bool get boolProperty => _boolProperty; + bool _boolProperty = true; + set boolProperty(bool value) { + if (value == _boolProperty) { + return; + } + _boolProperty = value; + notifyListeners(); + } +} From 49eac1fec6c710fd775d14728fc3461e11891ede Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 3 Nov 2023 16:55:12 -0500 Subject: [PATCH 2/2] [two_dimensional_scrollables] Add borderRadius support to TableSpanDecoration (#5184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of https://github.com/flutter/flutter/issues/134655 ![Screenshot 2023-10-19 at 3 01 17 PM](https://github.com/flutter/packages/assets/16964204/b185ce08-9be0-4771-83df-d79df66774b6) This may look wonky, but it is verifying all the combinations of consuming/not consuming the padding with decorations. One golden file to rule them all. 🤣 Thankfully we can refactor this very soon in - https://github.com/flutter/flutter/issues/136933 ![tableSpanDecoration defaultMainAxis](https://github.com/flutter/packages/assets/16964204/87d06e02-7708-47a6-b473-f62999a6c74b) --- .../two_dimensional_scrollables/CHANGELOG.md | 1 + .../lib/src/table_view/table_span.dart | 49 +++++++++++++----- .../tableSpanDecoration.defaultMainAxis.png | Bin 5003 -> 9516 bytes .../test/table_view/table_span_test.dart | 11 +++- .../test/table_view/table_test.dart | 16 +++--- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index 3a35f1e9c51..b61ddbd53a5 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Fixes bug where having one reversed axis caused incorrect painting of a pinned row. +* Adds support for BorderRadius in TableSpanDecorations. ## 0.0.4 diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart index fb6fb5c1f91..27d28d1028b 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart @@ -296,12 +296,19 @@ class TableSpanDecoration { const TableSpanDecoration({ this.border, this.color, + this.borderRadius, this.consumeSpanPadding = true, }); /// The border drawn around the span. final TableSpanBorder? border; + /// The radius by which the leading and trailing ends of a row or + /// column will be rounded. + /// + /// Applies to the [border] and [color] of the given [TableSpan]. + final BorderRadius? borderRadius; + /// The color to fill the bounds of the span with. final Color? color; @@ -364,15 +371,20 @@ class TableSpanDecoration { /// cells. void paint(TableSpanDecorationPaintDetails details) { if (color != null) { - details.canvas.drawRect( - details.rect, - Paint() - ..color = color! - ..isAntiAlias = false, - ); + final Paint paint = Paint() + ..color = color! + ..isAntiAlias = borderRadius != null; + if (borderRadius == null || borderRadius == BorderRadius.zero) { + details.canvas.drawRect(details.rect, paint); + } else { + details.canvas.drawRRect( + borderRadius!.toRRect(details.rect), + paint, + ); + } } if (border != null) { - border!.paint(details); + border!.paint(details, borderRadius); } } } @@ -416,24 +428,33 @@ class TableSpanBorder { /// cell representing the pinned column and separately with another /// [TableSpanDecorationPaintDetails.rect] containing all the other unpinned /// cells. - void paint(TableSpanDecorationPaintDetails details) { + void paint( + TableSpanDecorationPaintDetails details, + BorderRadius? borderRadius, + ) { final AxisDirection axisDirection = details.axisDirection; switch (axisDirectionToAxis(axisDirection)) { case Axis.horizontal: - paintBorder( - details.canvas, - details.rect, + final Border border = Border( top: axisDirection == AxisDirection.right ? leading : trailing, bottom: axisDirection == AxisDirection.right ? trailing : leading, ); - break; - case Axis.vertical: - paintBorder( + border.paint( details.canvas, details.rect, + borderRadius: borderRadius, + ); + break; + case Axis.vertical: + final Border border = Border( left: axisDirection == AxisDirection.down ? leading : trailing, right: axisDirection == AxisDirection.down ? trailing : leading, ); + border.paint( + details.canvas, + details.rect, + borderRadius: borderRadius, + ); break; } } diff --git a/packages/two_dimensional_scrollables/test/table_view/goldens/tableSpanDecoration.defaultMainAxis.png b/packages/two_dimensional_scrollables/test/table_view/goldens/tableSpanDecoration.defaultMainAxis.png index 44cb6497b63fbea7cc46d2de8579b4c4773e93c9..47c77cd4b1324e729866df80681079a79a1c2300 100644 GIT binary patch literal 9516 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 literal 5003 zcmeH~X;hPE7RMh5Dk__$qAVfUc4Vd&LuCnz5TsV{$l{2WH9!?*CuqVVkU&92WC;!} z1VIADf^1<8fd&#NN@^7ZG;A?!sXz!JKmrMYz{DBPnGbX3%$NBvr^n~ZbKZOI{oniE z=Y8M%do#|vIjQf{+XnzpKkIz@0ss&r0F=E{cPlIpVpF3Pn-b=NlOrJa8}JktDj3JJ z7gZH2Nj2y$0NNI3PoKINUo7Ax*K%TT%oRyHBZ2MXK(71#Lg=wl<)73J&0WloIE`4i z_F|@~y}j*)k=Yn&i$=Vt+6kqNu!zYxX?-s0i}B`i<+I+eQ3 z7l!D~U(ify(q#RaK2H=UR763+PQWO$j{zKt_5daQZKnZ}$Dyjfzefq)m z|F{FZ@C0y8PuJ%gffOBlH98z(pKv1ra|M~Z>^@qGd z#q&ON{mh+w76RQt$yx6!Y>a&+LuYCdZ@&wWv;HKMKMAS{o)S}j+N5qdBmoD^n^e}R z{(~??#KujC$Ei4L&m=#ZB*Vh>#V+7-zzFJ?)U}}v0|Of>&U>_A(c4e<0PG&6A=4G5 zId?_4M9WD1qxa@Rj8sHHd*;D*XEREmp^E64m zHb_I-k1g2nT*Cw<(u^~`@)47 zQ)qSAv+o49^_)$qXTo@%Q{_aVAg|K1rRSk80J+7IQOz9nuo&MDS|xxYlw#ogA&c2<{1 zrOzwz^h<(g2g{2LddoHZM@zYBd2Vo?}biSVOp@)80 z1359&9h&@nW+Bvl*vW9`2FkjraXO<+^o4Z;ZtiKiM85fko8DVfF~fB_M4!{vW|=j} zrN!NE)0yjiMM(6wHRE}n0=6{!V7WdiGT|C^P!X_ z{}shRKl%o}abKF!Y;^#}p9`SO1ny6zEUC^v`E=GrZSf8CJpEl9VRGhYf@}sW= z@e!yxD96{+e?a`%Kxau}h~UYwdNsz)r_~b`PD%j(TsQX)%1q|RXe7E}xCD}o$ItYe_%1Z|lG zO-_z^>A9J(FwYAoWVP(Pa8m{5&yR>Tn!~2fbF5n^OZclRSrk%sz(wIAtmX$ux;|0u zXXYlnZmuSQE|W(JkoGviSOFhJ5QGYEPR`_4mO&gKiZ|28BKk4iyrrG@Wob?4&QQX( zyr7l-Mr;7OuueYxCyf8n}Qft^5yQVu5wZ)QzKbwz^ z|76}?TQoZ|_;u#XHp}Iik_D)(L5V7O{E-nfI4Hy$C+hX(hfEghh9l~BTBRx8L?VLG z3r|j@BwmYJ?H_ACr$C6be?CE@N*gn zB}!97#`d5zQ8jmW#(Q&SKzG=$e5MmE$|sqj$fl*>7MzMRL_r!gqz)>1tg-cedudUz ziT!S;l6txvjxaq4qK`*)@6&#dFmPL=Q_a}~&Y=K*LhaVF?#>+8;4DJX$i!Tr&BaT?(P_OmjTE1}JmX1Q3;P^8FV*J8 z#yvA8^H-5Z1;n98#d$PCRrjQgrHOs3UrX&${0OsVZIvEM5-KMDg0ZkJJe?GA*>B$2 z_|u@vmwl2tPMFqP`Zcs@80wL>Z^zQ!-7Y@(D^PkpsynRfZ-dh^v8KHzCI?zoT_u%n zsZ(Fa3ya6kr^Rs2B`!#Pbv)e@o9tJm1^nOcXREubJ*4t)TN z7;70pXYR>+)(X?3>a*d7wcIp0Ybo@h-{vphJzog_M5RBpd$7@meibJerG)p!h@aHQ zjhNZWRp7wfH3ggd1BJ0<`~nudwbS%UBr|i*!ptsA_~31k9#IV_iVR_OmC1$%W-FK} za{(%$znE%D#!}`#H|5*o6f*nBK*?(t6c%1uA^z7Ger^$|isecE# Cj`MQ> diff --git a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart index 04618bad2b6..fea0d26e9a1 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart @@ -161,6 +161,7 @@ void main() { rect: rect, axisDirection: AxisDirection.down, ); + final BorderRadius radius = BorderRadius.circular(10.0); decoration.paint(details); expect(canvas.rect, rect); expect(canvas.paint.color, const Color(0xffff0000)); @@ -168,9 +169,13 @@ void main() { final TestTableSpanBorder border = TestTableSpanBorder( leading: const BorderSide(), ); - decoration = TableSpanDecoration(border: border); + decoration = TableSpanDecoration( + border: border, + borderRadius: radius, + ); decoration.paint(details); expect(border.details, details); + expect(border.radius, radius); }); } @@ -194,8 +199,10 @@ class TestCanvas implements Canvas { class TestTableSpanBorder extends TableSpanBorder { TestTableSpanBorder({super.leading}); TableSpanDecorationPaintDetails? details; + BorderRadius? radius; @override - void paint(TableSpanDecorationPaintDetails details) { + void paint(TableSpanDecorationPaintDetails details, BorderRadius? radius) { this.details = details; + this.radius = radius; } } diff --git a/packages/two_dimensional_scrollables/test/table_view/table_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index 1440a45582e..d910b772bce 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -1256,17 +1256,18 @@ void main() { // TODO(Piinks): Rewrite this to remove golden files from this repo when // mock_canvas is public - https://github.com/flutter/flutter/pull/131631 // * foreground, background, and precedence per mainAxis - // * Break out a separate test for padding decorations to validate paint - // rect calls + // * Break out a separate test for padding and radius decorations to + // validate paint rect calls TableView tableView = TableView.builder( rowCount: 2, columnCount: 2, columnBuilder: (int index) => TableSpan( extent: const FixedTableSpanExtent(200.0), padding: index == 0 ? const TableSpanPadding(trailing: 10) : null, - foregroundDecoration: const TableSpanDecoration( + foregroundDecoration: TableSpanDecoration( consumeSpanPadding: false, - border: TableSpanBorder( + borderRadius: BorderRadius.circular(10.0), + border: const TableSpanBorder( trailing: BorderSide( color: Colors.orange, width: 3, @@ -1276,14 +1277,16 @@ void main() { backgroundDecoration: TableSpanDecoration( // consumePadding true by default color: index.isEven ? Colors.red : null, + borderRadius: BorderRadius.circular(30.0), ), ), rowBuilder: (int index) => TableSpan( extent: const FixedTableSpanExtent(200.0), padding: index == 1 ? const TableSpanPadding(leading: 10) : null, - foregroundDecoration: const TableSpanDecoration( + foregroundDecoration: TableSpanDecoration( // consumePadding true by default - border: TableSpanBorder( + borderRadius: BorderRadius.circular(30.0), + border: const TableSpanBorder( leading: BorderSide( color: Colors.green, width: 3, @@ -1292,6 +1295,7 @@ void main() { ), backgroundDecoration: TableSpanDecoration( color: index.isOdd ? Colors.blue : null, + borderRadius: BorderRadius.circular(30.0), consumeSpanPadding: false, ), ),