Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 9560b2a

Browse files
authored
Prepare url_launcher for the Link widget (#3154)
1 parent fdc75a9 commit 9560b2a

7 files changed

Lines changed: 203 additions & 3 deletions

File tree

packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.0.9
2+
3+
* Laid the groundwork for introducing a Link widget.
4+
15
## 1.0.8
26

37
* Added webOnlyWindowName parameter
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2017 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:ui';
7+
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter/services.dart';
10+
11+
/// Signature for a function provided by the [Link] widget that instructs it to
12+
/// follow the link.
13+
typedef FollowLink = Future<void> Function();
14+
15+
/// Signature for a builder function passed to the [Link] widget to construct
16+
/// the widget tree under it.
17+
typedef LinkWidgetBuilder = Widget Function(
18+
BuildContext context,
19+
FollowLink followLink,
20+
);
21+
22+
/// Signature for a delegate function to build the [Link] widget.
23+
typedef LinkDelegate = Widget Function(LinkInfo linkWidget);
24+
25+
final MethodCodec _codec = const JSONMethodCodec();
26+
27+
/// Defines where a Link URL should be open.
28+
///
29+
/// This is a class instead of an enum to allow future customizability e.g.
30+
/// opening a link in a specific iframe.
31+
class LinkTarget {
32+
/// Const private constructor with a [debugLabel] to allow the creation of
33+
/// multiple distinct const instances.
34+
const LinkTarget._({this.debugLabel});
35+
36+
/// Used to distinguish multiple const instances of [LinkTarget].
37+
final String debugLabel;
38+
39+
/// Use the default target for each platform.
40+
///
41+
/// On Android, the default is [blank]. On the web, the default is [self].
42+
///
43+
/// iOS, on the other hand, defaults to [self] for web URLs, and [blank] for
44+
/// non-web URLs.
45+
static const defaultTarget = LinkTarget._(debugLabel: 'defaultTarget');
46+
47+
/// On the web, this opens the link in the same tab where the flutter app is
48+
/// running.
49+
///
50+
/// On Android and iOS, this opens the link in a webview within the app.
51+
static const self = LinkTarget._(debugLabel: 'self');
52+
53+
/// On the web, this opens the link in a new tab or window (depending on the
54+
/// browser and user configuration).
55+
///
56+
/// On Android and iOS, this opens the link in the browser or the relevant
57+
/// app.
58+
static const blank = LinkTarget._(debugLabel: 'blank');
59+
}
60+
61+
/// Encapsulates all the information necessary to build a Link widget.
62+
abstract class LinkInfo {
63+
/// Called at build time to construct the widget tree under the link.
64+
LinkWidgetBuilder get builder;
65+
66+
/// The destination that this link leads to.
67+
Uri get uri;
68+
69+
/// The target indicating where to open the link.
70+
LinkTarget get target;
71+
72+
/// Whether the link is disabled or not.
73+
bool get isDisabled;
74+
}
75+
76+
/// Pushes the [routeName] into Flutter's navigation system via a platform
77+
/// message.
78+
Future<ByteData> pushRouteNameToFramework(
79+
BuildContext context,
80+
String routeName, {
81+
@visibleForTesting bool debugForceRouter = false,
82+
}) {
83+
final Completer<ByteData> completer = Completer<ByteData>();
84+
if (debugForceRouter || _hasRouter(context)) {
85+
SystemNavigator.routeInformationUpdated(location: routeName);
86+
window.onPlatformMessage(
87+
'flutter/navigation',
88+
_codec.encodeMethodCall(
89+
MethodCall('pushRouteInformation', <dynamic, dynamic>{
90+
'location': routeName,
91+
'state': null,
92+
}),
93+
),
94+
completer.complete,
95+
);
96+
} else {
97+
window.onPlatformMessage(
98+
'flutter/navigation',
99+
_codec.encodeMethodCall(MethodCall('pushRoute', routeName)),
100+
completer.complete,
101+
);
102+
}
103+
return completer.future;
104+
}
105+
106+
bool _hasRouter(BuildContext context) {
107+
try {
108+
return Router.of(context) != null;
109+
} on AssertionError {
110+
// When a `Router` can't be found, an assertion error is thrown.
111+
return false;
112+
}
113+
}

packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ import 'dart:async';
77
import 'package:flutter/services.dart';
88
import 'package:meta/meta.dart' show required;
99

10+
import 'link.dart';
1011
import 'url_launcher_platform_interface.dart';
1112

1213
const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher');
1314

1415
/// An implementation of [UrlLauncherPlatform] that uses method channels.
1516
class MethodChannelUrlLauncher extends UrlLauncherPlatform {
17+
@override
18+
final LinkDelegate linkDelegate = null;
19+
1620
@override
1721
Future<bool> canLaunch(String url) {
1822
return _channel.invokeMethod<bool>(

packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66

77
import 'package:meta/meta.dart' show required;
88
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
9+
import 'package:url_launcher_platform_interface/link.dart';
910

1011
import 'method_channel_url_launcher.dart';
1112

@@ -38,6 +39,9 @@ abstract class UrlLauncherPlatform extends PlatformInterface {
3839
_instance = instance;
3940
}
4041

42+
/// The delegate used by the Link widget to build itself.
43+
LinkDelegate get linkDelegate;
44+
4145
/// Returns `true` if this platform is able to launch [url].
4246
Future<bool> canLaunch(String url) {
4347
throw UnimplementedError('canLaunch() has not been implemented.');

packages/url_launcher/url_launcher_platform_interface/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: A common platform interface for the url_launcher plugin.
33
homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface
44
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
55
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
6-
version: 1.0.8
6+
version: 1.0.9
77

88
dependencies:
99
flutter:
@@ -19,4 +19,4 @@ dev_dependencies:
1919

2020
environment:
2121
sdk: ">=2.1.0 <3.0.0"
22-
flutter: ">=1.9.1+hotfix.4 <2.0.0"
22+
flutter: ">=1.22.0 <2.0.0"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2017 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ui';
6+
7+
import 'package:mockito/mockito.dart';
8+
import 'package:flutter/services.dart';
9+
import 'package:flutter/widgets.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
12+
import 'package:url_launcher_platform_interface/link.dart';
13+
14+
final MethodCodec _codec = const JSONMethodCodec();
15+
16+
void main() {
17+
TestWidgetsFlutterBinding.ensureInitialized();
18+
19+
PlatformMessageCallback oldHandler;
20+
MethodCall lastCall;
21+
22+
setUp(() {
23+
oldHandler = window.onPlatformMessage;
24+
window.onPlatformMessage = (
25+
String name,
26+
ByteData data,
27+
PlatformMessageResponseCallback callback,
28+
) {
29+
lastCall = _codec.decodeMethodCall(data);
30+
callback(_codec.encodeSuccessEnvelope(true));
31+
};
32+
});
33+
34+
tearDown(() {
35+
window.onPlatformMessage = oldHandler;
36+
});
37+
38+
test('pushRouteNameToFramework() calls pushRoute when no Router', () async {
39+
await pushRouteNameToFramework(CustomBuildContext(), '/foo/bar');
40+
expect(
41+
lastCall,
42+
isMethodCall(
43+
'pushRoute',
44+
arguments: '/foo/bar',
45+
),
46+
);
47+
});
48+
49+
test(
50+
'pushRouteNameToFramework() calls pushRouteInformation when Router exists',
51+
() async {
52+
await pushRouteNameToFramework(
53+
CustomBuildContext(),
54+
'/foo/bar',
55+
debugForceRouter: true,
56+
);
57+
expect(
58+
lastCall,
59+
isMethodCall(
60+
'pushRouteInformation',
61+
arguments: <dynamic, dynamic>{
62+
'location': '/foo/bar',
63+
'state': null,
64+
},
65+
),
66+
);
67+
},
68+
);
69+
}
70+
71+
class CustomBuildContext<T> extends Mock implements BuildContext {}

packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
77
import 'package:flutter_test/flutter_test.dart';
88
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
99

10+
import 'package:url_launcher_platform_interface/link.dart';
1011
import 'package:url_launcher_platform_interface/method_channel_url_launcher.dart';
1112
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
1213

@@ -286,4 +287,7 @@ class UrlLauncherPlatformMock extends Mock
286287
class ImplementsUrlLauncherPlatform extends Mock
287288
implements UrlLauncherPlatform {}
288289

289-
class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {}
290+
class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {
291+
@override
292+
final LinkDelegate linkDelegate = null;
293+
}

0 commit comments

Comments
 (0)