Prepare url_launcher for the Link widget (#3154)
diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
index 768042b..10057e1 100644
--- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.9
+
+* Laid the groundwork for introducing a Link widget.
+
## 1.0.8
* Added webOnlyWindowName parameter
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart
new file mode 100644
index 0000000..425dc88
--- /dev/null
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart
@@ -0,0 +1,113 @@
+// Copyright 2017 The Chromium 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:async';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+/// Signature for a function provided by the [Link] widget that instructs it to
+/// follow the link.
+typedef FollowLink = Future<void> Function();
+
+/// Signature for a builder function passed to the [Link] widget to construct
+/// the widget tree under it.
+typedef LinkWidgetBuilder = Widget Function(
+ BuildContext context,
+ FollowLink followLink,
+);
+
+/// Signature for a delegate function to build the [Link] widget.
+typedef LinkDelegate = Widget Function(LinkInfo linkWidget);
+
+final MethodCodec _codec = const JSONMethodCodec();
+
+/// Defines where a Link URL should be open.
+///
+/// This is a class instead of an enum to allow future customizability e.g.
+/// opening a link in a specific iframe.
+class LinkTarget {
+ /// Const private constructor with a [debugLabel] to allow the creation of
+ /// multiple distinct const instances.
+ const LinkTarget._({this.debugLabel});
+
+ /// Used to distinguish multiple const instances of [LinkTarget].
+ final String debugLabel;
+
+ /// Use the default target for each platform.
+ ///
+ /// On Android, the default is [blank]. On the web, the default is [self].
+ ///
+ /// iOS, on the other hand, defaults to [self] for web URLs, and [blank] for
+ /// non-web URLs.
+ static const defaultTarget = LinkTarget._(debugLabel: 'defaultTarget');
+
+ /// On the web, this opens the link in the same tab where the flutter app is
+ /// running.
+ ///
+ /// On Android and iOS, this opens the link in a webview within the app.
+ static const self = LinkTarget._(debugLabel: 'self');
+
+ /// On the web, this opens the link in a new tab or window (depending on the
+ /// browser and user configuration).
+ ///
+ /// On Android and iOS, this opens the link in the browser or the relevant
+ /// app.
+ static const blank = LinkTarget._(debugLabel: 'blank');
+}
+
+/// Encapsulates all the information necessary to build a Link widget.
+abstract class LinkInfo {
+ /// Called at build time to construct the widget tree under the link.
+ LinkWidgetBuilder get builder;
+
+ /// The destination that this link leads to.
+ Uri get uri;
+
+ /// The target indicating where to open the link.
+ LinkTarget get target;
+
+ /// Whether the link is disabled or not.
+ bool get isDisabled;
+}
+
+/// Pushes the [routeName] into Flutter's navigation system via a platform
+/// message.
+Future<ByteData> pushRouteNameToFramework(
+ BuildContext context,
+ String routeName, {
+ @visibleForTesting bool debugForceRouter = false,
+}) {
+ final Completer<ByteData> completer = Completer<ByteData>();
+ if (debugForceRouter || _hasRouter(context)) {
+ SystemNavigator.routeInformationUpdated(location: routeName);
+ window.onPlatformMessage(
+ 'flutter/navigation',
+ _codec.encodeMethodCall(
+ MethodCall('pushRouteInformation', <dynamic, dynamic>{
+ 'location': routeName,
+ 'state': null,
+ }),
+ ),
+ completer.complete,
+ );
+ } else {
+ window.onPlatformMessage(
+ 'flutter/navigation',
+ _codec.encodeMethodCall(MethodCall('pushRoute', routeName)),
+ completer.complete,
+ );
+ }
+ return completer.future;
+}
+
+bool _hasRouter(BuildContext context) {
+ try {
+ return Router.of(context) != null;
+ } on AssertionError {
+ // When a `Router` can't be found, an assertion error is thrown.
+ return false;
+ }
+}
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart
index f87630e..ac5bfa2 100644
--- a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart
@@ -7,6 +7,7 @@
import 'package:flutter/services.dart';
import 'package:meta/meta.dart' show required;
+import 'link.dart';
import 'url_launcher_platform_interface.dart';
const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher');
@@ -14,6 +15,9 @@
/// An implementation of [UrlLauncherPlatform] that uses method channels.
class MethodChannelUrlLauncher extends UrlLauncherPlatform {
@override
+ final LinkDelegate linkDelegate = null;
+
+ @override
Future<bool> canLaunch(String url) {
return _channel.invokeMethod<bool>(
'canLaunch',
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
index 1de5742..75002ff 100644
--- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
@@ -6,6 +6,7 @@
import 'package:meta/meta.dart' show required;
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+import 'package:url_launcher_platform_interface/link.dart';
import 'method_channel_url_launcher.dart';
@@ -38,6 +39,9 @@
_instance = instance;
}
+ /// The delegate used by the Link widget to build itself.
+ LinkDelegate get linkDelegate;
+
/// Returns `true` if this platform is able to launch [url].
Future<bool> canLaunch(String url) {
throw UnimplementedError('canLaunch() has not been implemented.');
diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
index 0c40962..ce0fdd9 100644
--- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
@@ -3,7 +3,7 @@
homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 1.0.8
+version: 1.0.9
dependencies:
flutter:
@@ -19,4 +19,4 @@
environment:
sdk: ">=2.1.0 <3.0.0"
- flutter: ">=1.9.1+hotfix.4 <2.0.0"
+ flutter: ">=1.22.0 <2.0.0"
diff --git a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart
new file mode 100644
index 0000000..99a885c
--- /dev/null
+++ b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart
@@ -0,0 +1,71 @@
+// Copyright 2017 The Chromium 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:ui';
+
+import 'package:mockito/mockito.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:url_launcher_platform_interface/link.dart';
+
+final MethodCodec _codec = const JSONMethodCodec();
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ PlatformMessageCallback oldHandler;
+ MethodCall lastCall;
+
+ setUp(() {
+ oldHandler = window.onPlatformMessage;
+ window.onPlatformMessage = (
+ String name,
+ ByteData data,
+ PlatformMessageResponseCallback callback,
+ ) {
+ lastCall = _codec.decodeMethodCall(data);
+ callback(_codec.encodeSuccessEnvelope(true));
+ };
+ });
+
+ tearDown(() {
+ window.onPlatformMessage = oldHandler;
+ });
+
+ test('pushRouteNameToFramework() calls pushRoute when no Router', () async {
+ await pushRouteNameToFramework(CustomBuildContext(), '/foo/bar');
+ expect(
+ lastCall,
+ isMethodCall(
+ 'pushRoute',
+ arguments: '/foo/bar',
+ ),
+ );
+ });
+
+ test(
+ 'pushRouteNameToFramework() calls pushRouteInformation when Router exists',
+ () async {
+ await pushRouteNameToFramework(
+ CustomBuildContext(),
+ '/foo/bar',
+ debugForceRouter: true,
+ );
+ expect(
+ lastCall,
+ isMethodCall(
+ 'pushRouteInformation',
+ arguments: <dynamic, dynamic>{
+ 'location': '/foo/bar',
+ 'state': null,
+ },
+ ),
+ );
+ },
+ );
+}
+
+class CustomBuildContext<T> extends Mock implements BuildContext {}
diff --git a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart
index 628ab48..d88f53a 100644
--- a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart
@@ -7,6 +7,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+import 'package:url_launcher_platform_interface/link.dart';
import 'package:url_launcher_platform_interface/method_channel_url_launcher.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
@@ -286,4 +287,7 @@
class ImplementsUrlLauncherPlatform extends Mock
implements UrlLauncherPlatform {}
-class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {}
+class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {
+ @override
+ final LinkDelegate linkDelegate = null;
+}