diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 068650be6d53..06fa5e7ced7d 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.12 + +* Fixed call to `setState` after dispose. + ## 2.0.11 * Minor fixes for new analysis options. diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart index 3b75e0556686..6b19861c5ce5 100644 --- a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart @@ -123,6 +123,36 @@ void main() { final html.Element anchor = _findSingleAnchor(); expect(anchor.hasAttribute('href'), false); }); + + testWidgets('can be created and disposed', (WidgetTester tester) async { + final Uri uri = Uri.parse('http://foobar'); + const int itemCount = 500; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ListView.builder( + itemCount: itemCount, + itemBuilder: (_, int index) => WebLinkDelegate(TestLinkInfo( + uri: uri, + target: LinkTarget.defaultTarget, + builder: (BuildContext context, FollowLink? followLink) => + Text('#$index', textAlign: TextAlign.center), + )), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + await tester.scrollUntilVisible( + find.text('#${itemCount - 1}'), + 2500, + maxScrolls: 1000, + ); + }); }); } diff --git a/packages/url_launcher/url_launcher_web/lib/src/link.dart b/packages/url_launcher/url_launcher_web/lib/src/link.dart index eccd9aef80e3..b0adc00185ea 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/link.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -76,7 +76,7 @@ class WebLinkDelegateState extends State { child: PlatformViewLink( viewType: linkViewType, onCreatePlatformView: (PlatformViewCreationParams params) { - _controller = LinkViewController.fromParams(params, context); + _controller = LinkViewController.fromParams(params); return _controller ..setUri(widget.link.uri) ..setTarget(widget.link.target); @@ -100,7 +100,7 @@ class WebLinkDelegateState extends State { /// Controls link views. class LinkViewController extends PlatformViewController { /// Creates a [LinkViewController] instance with the unique [viewId]. - LinkViewController(this.viewId, this.context) { + LinkViewController(this.viewId) : _element = _makeElement(viewId) { if (_instances.isEmpty) { // This is the first controller being created, attach the global click // listener. @@ -113,14 +113,28 @@ class LinkViewController extends PlatformViewController { /// platform view [params]. factory LinkViewController.fromParams( PlatformViewCreationParams params, - BuildContext context, - ) { - final int viewId = params.id; - final LinkViewController controller = LinkViewController(viewId, context); - controller._initialize().then((_) { + ) => + LinkViewController(params.id).._asyncInitialize(params); + + Future _asyncInitialize(PlatformViewCreationParams params) async { + try { + await SystemChannels.platform_views + .invokeMethod('create', { + 'id': viewId, + 'viewType': linkViewType, + }); + if (_isDisposed) { + return; + } params.onPlatformViewCreated(viewId); - }); - return controller; + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'url_launcher', + context: ErrorDescription('while initializing view $viewId'), + )); + } } static final Map _instances = @@ -159,33 +173,9 @@ class LinkViewController extends PlatformViewController { @override final int viewId; - /// The context of the [Link] widget that created this controller. - final BuildContext context; - - late html.Element _element; - - bool get _isInitialized => _element != null; + html.Element _element; - Future _initialize() async { - _element = html.Element.tag('a'); - setProperty(_element, linkViewIdProperty, viewId); - _element.style - ..opacity = '0' - ..display = 'block' - ..width = '100%' - ..height = '100%' - ..cursor = 'unset'; - - // This is recommended on MDN: - // - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target - _element.setAttribute('rel', 'noreferrer noopener'); - - final Map args = { - 'id': viewId, - 'viewType': linkViewType, - }; - await SystemChannels.platform_views.invokeMethod('create', args); - } + bool _isDisposed = false; void _onDomClick(html.MouseEvent event) { final bool isHitTested = _hitTestedViewId == viewId; @@ -208,7 +198,7 @@ class LinkViewController extends PlatformViewController { // browser handle it. event.preventDefault(); final String routeName = _uri.toString(); - pushRouteNameToFramework(context, routeName); + pushRouteNameToFramework(null, routeName); } Uri? _uri; @@ -217,7 +207,6 @@ class LinkViewController extends PlatformViewController { /// /// When Uri is null, the `href` attribute of the link is removed. void setUri(Uri? uri) { - assert(_isInitialized); _uri = uri; if (uri == null) { _element.removeAttribute('href'); @@ -234,7 +223,6 @@ class LinkViewController extends PlatformViewController { /// Set the [LinkTarget] value for this link. void setTarget(LinkTarget target) { - assert(_isInitialized); _element.setAttribute('target', _getHtmlTarget(target)); } @@ -263,18 +251,49 @@ class LinkViewController extends PlatformViewController { } @override - Future dispose() async { - if (_isInitialized) { + Future dispose() { + if (!_isDisposed) { assert(_instances[viewId] == this); _instances.remove(viewId); + _isDisposed = true; + return _asyncDispose(); + } + return Future.value(); + } + + Future _asyncDispose() async { + try { if (_instances.isEmpty) { await _clickSubscription.cancel(); } await SystemChannels.platform_views.invokeMethod('dispose', viewId); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'url_launcher', + context: ErrorDescription('while disposing view $viewId'), + )); } } } +html.Element _makeElement(int viewId) { + final html.Element element = html.Element.tag('a'); + setProperty(element, linkViewIdProperty, viewId); + element.style + ..opacity = '0' + ..display = 'block' + ..width = '100%' + ..height = '100%' + ..cursor = 'unset'; + + // This is recommended on MDN: + // - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target + element.setAttribute('rel', 'noreferrer noopener'); + return element; +} + /// Finds the view id of the DOM element targeted by the [event]. int? getViewIdFromTarget(html.Event event) { final html.Element? linkElement = getLinkElementFromTarget(event); diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index cef323035379..d0e0fa905d57 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.11 +version: 2.0.12 environment: sdk: ">=2.12.0 <3.0.0"