From 7c0300a3588d91542366d9efadda26fa2c9d9224 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 27 Jan 2021 09:20:56 -0800 Subject: [PATCH] [flutter] throw more specific error messages if a lerp'd type does not conform to Tween API (#74684) --- packages/flutter/lib/src/animation/tween.dart | 45 +++++++++ .../flutter/test/animation/tween_test.dart | 98 +++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/packages/flutter/lib/src/animation/tween.dart b/packages/flutter/lib/src/animation/tween.dart index 773043039b..803b00b7a2 100644 --- a/packages/flutter/lib/src/animation/tween.dart +++ b/packages/flutter/lib/src/animation/tween.dart @@ -256,6 +256,51 @@ class Tween extends Animatable { T lerp(double t) { assert(begin != null); assert(end != null); + assert(() { + // Assertions that attempt to catch common cases of tweening types + // that do not conform to the Tween requirements. + dynamic? result; + try { + result = begin + (end - begin) * t; + result as T; + return true; + } on NoSuchMethodError { + throw FlutterError.fromParts([ + ErrorSummary('Cannot lerp between "$begin" and "$end".'), + ErrorDescription( + 'The type ${begin.runtimeType} might not fully implement `+`, `-`, and/or `*`. ' + 'See "Types with special considerations" at https://api.flutter.dev/flutter/animation/Tween-class.html ' + 'for more information.', + ), + if (begin is Color || end is Color) + ErrorHint('To lerp colors, consider ColorTween instead.') + else if (begin is Rect || end is Rect) + ErrorHint('To lerp rects, consider RectTween instead.') + else + ErrorHint( + 'There may be a dedicated "${begin.runtimeType}Tween" for this type, ' + 'or you may need to create one.' + ), + ]); + } on TypeError { + throw FlutterError.fromParts([ + ErrorSummary('Cannot lerp between "$begin" and "$end".'), + ErrorDescription( + 'The type ${begin.runtimeType} returned a ${result.runtimeType} after ' + 'multiplication with a double value. ' + 'See "Types with special considerations" at https://api.flutter.dev/flutter/animation/Tween-class.html ' + 'for more information.', + ), + if (begin is int || end is int) + ErrorHint('To lerp int values, consider IntTween or StepTween instead.') + else + ErrorHint( + 'There may be a dedicated "${begin.runtimeType}Tween" for this type, ' + 'or you may need to create one.' + ), + ]); + } + }()); return begin + (end - begin) * t as T; } diff --git a/packages/flutter/test/animation/tween_test.dart b/packages/flutter/test/animation/tween_test.dart index 94cbed3176..1f5930b6d0 100644 --- a/packages/flutter/test/animation/tween_test.dart +++ b/packages/flutter/test/animation/tween_test.dart @@ -7,7 +7,105 @@ import 'package:flutter/animation.dart'; import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart'; +const String kApiDocsLink = 'See "Types with special considerations" at https://api.flutter.dev/flutter/animation/Tween-class.html for more information.'; + void main() { + test('throws flutter error when tweening types that do not fully satisfy tween requirements - Object', () { + final Tween objectTween = Tween( + begin: Object(), + end: Object(), + ); + + FlutterError? error; + try { + objectTween.transform(0.1); + } on FlutterError catch (err) { + error = err; + } + + if (error == null) { + fail('Expected Tween.transform to throw a FlutterError'); + } + + expect(error.diagnostics.map((DiagnosticsNode node) => node.toString()), [ + 'Cannot lerp between "Instance of \'Object\'" and "Instance of \'Object\'".', + 'The type Object might not fully implement `+`, `-`, and/or `*`. $kApiDocsLink', + 'There may be a dedicated "ObjectTween" for this type, or you may need to create one.' + ]); + }); + + test('throws flutter error when tweening types that do not fully satisfy tween requirements - Color', () { + final Tween colorTween = Tween( + begin: const Color(0xFF000000), + end: const Color(0xFFFFFFFF), + ); + + FlutterError? error; + try { + colorTween.transform(0.1); + } on FlutterError catch (err) { + error = err; + } + + if (error == null) { + fail('Expected Tween.transform to throw a FlutterError'); + } + + expect(error.diagnostics.map((DiagnosticsNode node) => node.toString()), [ + 'Cannot lerp between "Color(0xff000000)" and "Color(0xffffffff)".', + 'The type Color might not fully implement `+`, `-`, and/or `*`. $kApiDocsLink', + 'To lerp colors, consider ColorTween instead.', + ]); + }); + + test('throws flutter error when tweening types that do not fully satisfy tween requirements - Rect', () { + final Tween rectTween = Tween( + begin: const Rect.fromLTWH(0, 0, 10, 10), + end: const Rect.fromLTWH(2, 2, 2, 2) + ); + + FlutterError? error; + try { + rectTween.transform(0.1); + } on FlutterError catch (err) { + error = err; + } + + if (error == null) { + fail('Expected Tween.transform to throw a FlutterError'); + } + + expect(error.diagnostics.map((DiagnosticsNode node) => node.toString()), [ + 'Cannot lerp between "Rect.fromLTRB(0.0, 0.0, 10.0, 10.0)" and "Rect.fromLTRB(2.0, 2.0, 4.0, 4.0)".', + 'The type Rect might not fully implement `+`, `-`, and/or `*`. $kApiDocsLink', + 'To lerp rects, consider RectTween instead.', + ]); + }); + + test('throws flutter error when tweening types that do not fully satisfy tween requirements - int', () { + final Tween colorTween = Tween( + begin: 0, + end: 1, + ); + + FlutterError? error; + try { + colorTween.transform(0.1); + } on FlutterError catch (err) { + error = err; + } + + if (error == null) { + fail('Expected Tween.transform to throw a FlutterError'); + } + + expect(error.diagnostics.map((DiagnosticsNode node) => node.toString()), [ + 'Cannot lerp between "0" and "1".', + 'The type int returned a double after multiplication with a double value. $kApiDocsLink', + 'To lerp int values, consider IntTween or StepTween instead.', + ]); + }); + test('Can chain tweens', () { final Tween tween = Tween(begin: 0.30, end: 0.50); expect(tween, hasOneLineDescription);