Skip to content

Commit

Permalink
Merge pull request #215 from droibit/feature/may_launch_urls
Browse files Browse the repository at this point in the history
Enhance Custom Tabs functionality with mayLaunchUrls method
  • Loading branch information
droibit authored Nov 21, 2024
2 parents 24f1a4d + 4ebf53a commit 9d2e6c4
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 21 deletions.
65 changes: 61 additions & 4 deletions flutter_custom_tabs/lib/src/launcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Future<void> closeCustomTabs() {
///
/// **Note:** It's recommended to call [invalidateSession] when the session is no longer needed to release resources.
///
/// Returns a [CustomTabsSession] which can be used when launching a URL with a specific session.
/// Returns a [CustomTabsSession] which can be used when launching a URL with a specific session on Android.
///
/// ### Example
///
Expand Down Expand Up @@ -139,7 +139,7 @@ Future<CustomTabsSession> warmupCustomTabs({
return session as CustomTabsSession? ?? const CustomTabsSession(null);
}

/// Notifies the browser of a potential URL that might be launched later,
/// Tells the browser of a potential URL that might be launched later,
/// improving performance when the URL is actually launched.
///
/// On **Android**, this method pre-fetches the web page at the specified URL.
Expand All @@ -152,6 +152,11 @@ Future<CustomTabsSession> warmupCustomTabs({
/// Use this method when you expect to present [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) soon.
/// Many HTTP servers time out connections after a few minutes.
/// After a timeout, prewarming delivers less performance benefit.
/// This feature is available on iOS 15 and above.
///
/// On **Web**, this method does nothing.
///
/// Returns a [SafariViewPrewarmingSession] for disconnecting the connection on iOS.
///
/// **Note:** It's crucial to call [invalidateSession] to release resources and properly dispose of the session when it is no longer needed.
///
Expand All @@ -168,9 +173,58 @@ Future<CustomTabsSession> warmupCustomTabs({
Future<SafariViewPrewarmingSession> mayLaunchUrl(
Uri url, {
CustomTabsSession? customTabsSession,
}) async {
return mayLaunchUrls(
[url],
customTabsSession: customTabsSession,
);
}

/// Tells the browser of potential URLs that might be launched later,
/// improving performance when the URL is actually launched.
///
/// On **Android**, this method pre-fetches the web page at the specified URLs.
/// This can improve page load time when the URL is launched later using [launchUrl].
/// [urls] to be used for launching. The first URL in the list is considered the most likely
/// and should be specified first. Optionally, additional URLs can be provided in decreasing priority order.
/// These additional URLs are treated as less likely than the first one and may be ignored.
/// Note that all previous calls to this method will be deprioritized.
/// For more details, see
/// [Warm-up and pre-fetch: using the Custom Tabs Service](https://developer.chrome.com/docs/android/custom-tabs/guide-warmup-prefetch).
///
/// On **iOS**, this method uses a best-effort approach to prewarming connections,
/// but may delay or drop requests based on the volume of requests made by your app.
/// Use this method when you expect to present [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) soon.
/// Many HTTP servers time out connections after a few minutes.
/// After a timeout, prewarming delivers less performance benefit.
/// This feature is available on iOS 15 and above.
///
/// On **Web**, this method does nothing.
///
/// Returns a [SafariViewPrewarmingSession] for disconnecting the connection on iOS.
///
/// **Note:** It's crucial to call [invalidateSession] to release resources and properly dispose of the session when it is no longer needed.
///
/// ### Example
///
/// ```dart
/// final prewarmingSession = await mayLaunchUrls(
/// [
/// Uri.parse('https://flutter.dev'),
/// Uri.parse('https://dart.dev'),
/// ],
/// options: const CustomTabsSessionOptions(prefersDefaultBrowser: true),
/// );
///
/// // Invalidates the session when the originating screen is disposed or in other cases where the session should be invalidated.
/// await invalidateSession(prewarmingSession);
/// ```
Future<SafariViewPrewarmingSession> mayLaunchUrls(
List<Uri> urls, {
CustomTabsSession? customTabsSession,
}) async {
final session = await CustomTabsPlatform.instance.mayLaunch(
[_requireWebUrl(url)],
urls.map(_requireWebUrl).toList(),
session: switch (defaultTargetPlatform) {
TargetPlatform.android => customTabsSession,
_ => null,
Expand All @@ -182,7 +236,10 @@ Future<SafariViewPrewarmingSession> mayLaunchUrl(

/// Invalidates a session to release resources and properly dispose of it.
///
/// Use this method to invalidate a session that was created using [warmupCustomTabs] or [mayLaunchUrl] when it is no longer needed.
/// Use this method to invalidate a session that was created using one of the following methods when it is no longer needed:
/// - [warmupCustomTabs]
/// - [mayLaunchUrl]
/// - [mayLaunchUrls]
Future<void> invalidateSession(PlatformSession session) {
return CustomTabsPlatform.instance.invalidate(session);
}
Expand Down
67 changes: 62 additions & 5 deletions flutter_custom_tabs/test/launcher_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ void main() {
final mock = MockCustomTabsPlatform();
setUp(() => {CustomTabsPlatform.instance = mock});

test('launchUrl() launch with non-web URL', () async {
test('launchUrl() throws ArgumentError when URL scheme is not http or https',
() async {
final url = Uri.parse('file:/home');
expect(
() => launchUrl(url),
Expand Down Expand Up @@ -128,10 +129,12 @@ void main() {
expect(mock.warmupCalled, isTrue);
});

test('mayLaunchUrl() launch with non-web URL', () async {
test(
'mayLaunchUrl() throws ArgumentError when URL scheme is not http or https',
() async {
final url = Uri.parse('file:/home');
expect(
() => launchUrl(url),
() => mayLaunchUrl(url),
throwsA(isA<ArgumentError>()),
);
});
Expand Down Expand Up @@ -166,9 +169,63 @@ void main() {
customTabsSession: null,
prewarmingSession: prewarmingSession);

final actualSession = await mayLaunchUrl(
Uri.parse(url),
final actualSession = await mayLaunchUrl(Uri.parse(url));
expect(actualSession.id, prewarmingSession.id);
expect(mock.mayLaunchCalled, isTrue);
});

test(
'mayLaunchUrls() throws ArgumentError when URL scheme is not http or https',
() async {
final urls = [
Uri.parse('https://example.com/'),
Uri.parse('file:/home'),
];
expect(
() => mayLaunchUrls(urls),
throwsA(isA<ArgumentError>()),
);
});

test(
'mayLaunchUrls() invoke method "mayLaunch" with CustomTabsSession on Android',
() async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;

const urls = [
'http://example.com/',
'http://flutter.dev/',
];
const customTabsSession = CustomTabsSession('com.example.browser');
mock.setMayLaunchExpectations(
urls: urls,
customTabsSession: customTabsSession,
);

final actualSession = await mayLaunchUrls(
urls.map(Uri.parse).toList(),
customTabsSession: customTabsSession,
);
expect(actualSession.id, isNull);
expect(mock.mayLaunchCalled, isTrue);
});

test('mayLaunchUrls() invoke method "mayLaunch" with null on iOS', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;

const urls = [
'http://example.com/',
'http://flutter.dev/',
];
const prewarmingSession = SafariViewPrewarmingSession('test-session-id');
mock.setMayLaunchExpectations(
urls: urls,
customTabsSession: null,
prewarmingSession: prewarmingSession,
);

final actualSession = await mayLaunchUrls(
urls.map(Uri.parse).toList(),
);
expect(actualSession.id, prewarmingSession.id);
expect(mock.mayLaunchCalled, isTrue);
Expand Down
30 changes: 23 additions & 7 deletions flutter_custom_tabs_android/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ class _MyAppState extends State<MyApp> {
void initState() {
super.initState();

// After warming up, the session might not be established immediately, so we wait for a short period.
// After warming up, the session might not be established immediately, so we wait for a short period.
final session = widget.session;
Future.delayed(const Duration(seconds: 1), () async {
await CustomTabsPlatform.instance.mayLaunch(
['https://flutter.dev'],
[
'https://flutter.dev',
'https://dart.dev',
],
session: session,
);
});
Expand Down Expand Up @@ -94,9 +97,21 @@ class _MyAppState extends State<MyApp> {
child: const Text('Show flutter.dev in external browser'),
),
FilledButton.tonal(
onPressed: () => _launchURLWithSession(context, widget.session),
onPressed: () => _launchURLWithSession(
context,
uri: 'https://flutter.dev',
session: widget.session,
),
child: const Text('Show flutter.dev with session'),
),
FilledButton.tonal(
onPressed: () => _launchURLWithSession(
context,
uri: 'https://dart.dev',
session: widget.session,
),
child: const Text('Show dart.dev with session'),
),
],
),
),
Expand Down Expand Up @@ -227,13 +242,14 @@ Future<void> _launchInExternalBrowser() async {
}

Future<void> _launchURLWithSession(
BuildContext context,
CustomTabsSession session,
) async {
BuildContext context, {
required String uri,
required CustomTabsSession session,
}) async {
final theme = Theme.of(context);
try {
await CustomTabsPlatform.instance.launch(
'https://flutter.dev',
uri,
customTabsOptions: CustomTabsOptions(
colorSchemes: CustomTabsColorSchemes.defaults(
toolbarColor: theme.colorScheme.surface,
Expand Down
19 changes: 14 additions & 5 deletions flutter_custom_tabs_ios/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ class _MyAppState extends State<MyApp> {

Future(() async {
_session = await CustomTabsPlatform.instance.mayLaunch(
['https://flutter.dev'],
session: null,
[
'https://flutter.dev',
'https://dart.dev',
],
);
debugPrint('Warm up session: $_session');
});
Expand Down Expand Up @@ -64,9 +66,13 @@ class _MyAppState extends State<MyApp> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
children: <Widget>[
FilledButton(
onPressed: () => _launchURL(context),
onPressed: () => _launchURL('https://flutter.dev', context),
child: const Text('Show flutter.dev'),
),
FilledButton(
onPressed: () => _launchURL('https://dart.dev', context),
child: const Text('Show dart.dev'),
),
FilledButton(
onPressed: () => _launchURLInBottomSheet(context),
child: const Text('Show flutter.dev in bottom sheet'),
Expand All @@ -91,11 +97,14 @@ class _MyAppState extends State<MyApp> {
}
}

Future<void> _launchURL(BuildContext context) async {
Future<void> _launchURL(
String url,
BuildContext context,
) async {
final theme = Theme.of(context);
try {
await CustomTabsPlatform.instance.launch(
'https://flutter.dev',
url,
safariVCOptions: SafariViewControllerOptions(
preferredBarTintColor: theme.colorScheme.surface,
preferredControlTintColor: theme.colorScheme.onSurface,
Expand Down

0 comments on commit 9d2e6c4

Please sign in to comment.