Skip to content

Commit

Permalink
Merge pull request #211 from droibit/feature/warmup
Browse files Browse the repository at this point in the history
Add warmup and invalidate session functionality
  • Loading branch information
droibit authored Nov 16, 2024
2 parents 2111acc + 12f4af3 commit 87cf819
Show file tree
Hide file tree
Showing 44 changed files with 1,632 additions and 118 deletions.
2 changes: 1 addition & 1 deletion flutter_custom_tabs/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ if (flutterVersionName == null) {

android {
namespace "com.github.droibit.flutter.plugins.customtabs.example"
compileSdk flutter.compileSdkVersion
compileSdk 34 // flutter.compileSdkVersion
ndkVersion flutter.ndkVersion

defaultConfig {
Expand Down
47 changes: 45 additions & 2 deletions flutter_custom_tabs/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs_lite.dart' as lite;

void main() => runApp(const MyApp());
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final session = await warmupCustomTabs(
options: const CustomTabsSessionOptions(prefersDefaultBrowser: true),
);
debugPrint('Warm up session: $session');
runApp(MyApp(session));
}

class MyApp extends StatelessWidget {
const MyApp({super.key});
final CustomTabsSession _session;

const MyApp(
CustomTabsSession session, {
super.key,
}) : _session = session;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -75,6 +87,10 @@ class MyApp extends StatelessWidget {
onPressed: () => _launchInExternalBrowser(),
child: const Text('Show flutter.dev in external browser'),
),
FilledButton.tonal(
onPressed: () => _launchURLWithSession(context, _session),
child: const Text('Show flutter.dev with session'),
),
],
),
),
Expand Down Expand Up @@ -297,3 +313,30 @@ Future<void> _launchInExternalBrowser() async {
debugPrint(e.toString());
}
}

Future<void> _launchURLWithSession(
BuildContext context,
CustomTabsSession session,
) async {
final theme = Theme.of(context);
try {
await launchUrl(
Uri.parse('https://flutter.dev'),
customTabsOptions: CustomTabsOptions(
colorSchemes: CustomTabsColorSchemes.defaults(
toolbarColor: theme.colorScheme.surface,
navigationBarColor: theme.colorScheme.surface,
),
urlBarHidingEnabled: true,
showTitle: true,
browser: CustomTabsBrowserConfiguration.session(session),
),
safariVCOptions: SafariViewControllerOptions(
preferredBarTintColor: theme.colorScheme.surface,
preferredControlTintColor: theme.colorScheme.onSurface,
),
);
} catch (e) {
debugPrint(e.toString());
}
}
2 changes: 1 addition & 1 deletion flutter_custom_tabs/lib/flutter_custom_tabs.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export 'package:flutter_custom_tabs_android/flutter_custom_tabs_android.dart'
hide CustomTabsPluginAndroid;
hide CustomTabsPluginAndroid, CustomTabsOptionsConverter;
export 'package:flutter_custom_tabs_ios/flutter_custom_tabs_ios.dart'
hide CustomTabsPluginIOS;

Expand Down
91 changes: 79 additions & 12 deletions flutter_custom_tabs/lib/src/launcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ import 'package:flutter_custom_tabs_android/flutter_custom_tabs_android.dart';
import 'package:flutter_custom_tabs_ios/flutter_custom_tabs_ios.dart';
import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platform_interface.dart';

/// Passes [url] with options to the underlying platform for launching a custom tab.
/// Launches a web URL using a custom tab or browser, with various customization options.
///
/// - On Android, the appearance and behavior of [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/) can be customized using the [customTabsOptions] parameter.
/// - On iOS, the appearance and behavior of [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) can be customized using the [safariVCOptions] parameter.
/// - For web, customization options are not available.
/// The [launchUrl] function provides a way to open web content within your app using
/// a customizable in-app browser experience on Android and iOS platforms.
///
/// If [customTabsOptions] or [safariVCOptions] are `null`, the URL will be launched in an external browser on mobile platforms.
/// ### Supported Platforms
///
/// Example of launching Custom Tabs:
/// - **Android**: Uses [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/)
/// to display web content within your app. Customize the appearance and behavior
/// using the [customTabsOptions] parameter.
/// - **iOS**: Uses [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller)
/// to present web content. Customize using the [safariVCOptions] parameter.
/// - **Web**: Customization options are not available; the URL will be opened in a new browser tab.
///
/// ### Examples
///
/// **Launch a URL with customization:**
///
/// ```dart
/// final theme = ...;
Expand Down Expand Up @@ -42,7 +50,7 @@ import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platf
/// }
/// ```
///
/// Example of launching an external browser:
/// **Launch a URL in the external browser:**
///
/// ```dart
/// try {
Expand All @@ -51,6 +59,13 @@ import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platf
/// // An exception is thrown if browser app is not installed on Android device.
/// }
/// ```
///
/// ### Notes
///
/// - The URL must have an `http` or `https` scheme; otherwise, a [PlatformException] is thrown.
/// - Use [closeCustomTabs] to programmatically close the custom tab if needed.
/// - Make sure to call the [warmupCustomTabs] function before launching the URL to improve performance.
///
Future<void> launchUrl(
Uri url, {
bool prefersDeepLink = false,
Expand All @@ -72,12 +87,64 @@ Future<void> launchUrl(
);
}

/// Closes all custom tabs that were opened earlier by "launchUrl".
/// Closes all Custom Tabs that were opened earlier by [launchUrl].
///
/// Availability:
/// - Android: **SDK 23+**
/// - iOS: Any
/// - Web: Not supported
/// **Platform Availability:**
/// - **Android:** Supported on SDK 23 (Android 6.0) and above.
/// - **iOS:** All versions.
/// - **Web:** Not supported.
Future<void> closeCustomTabs() async {
await CustomTabsPlatform.instance.closeAllIfPossible();
}

/// Pre-warms the Custom Tabs browser process, potentially improving performance when launching a URL.
///
/// On **Android**, calling `warmupCustomTabs()` initializes the Custom Tabs service,
/// causing the browser process to start in the background even before the user clicks on a link.
/// This can save up to **700ms** when opening a link. If [options] are not provided,
/// the default browser to warm up is **Chrome**.
///
/// 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 **other platforms**, this method does nothing.
///
/// **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.
///
/// ### Example
///
/// ```dart
/// final session = await warmupCustomTabs(
/// options: const CustomTabsSessionOptions(prefersDefaultBrowser: true),
/// );
/// debugPrint('Warm up session: $session');
///
/// await launchUrl(
/// Uri.parse('https://flutter.dev'),
/// customTabsOptions: CustomTabsOptions(
/// colorSchemes: CustomTabsColorSchemes.defaults(
/// toolbarColor: theme.colorScheme.surface,
/// ),
/// urlBarHidingEnabled: true,
/// showTitle: true,
/// browser: CustomTabsBrowserConfiguration.session(session),
/// ),
/// safariVCOptions: SafariViewControllerOptions(
/// preferredBarTintColor: theme.colorScheme.surface,
/// preferredControlTintColor: theme.colorScheme.onSurface,
/// barCollapsingEnabled: true,
/// ),
/// );
/// ```
Future<CustomTabsSession> warmupCustomTabs({
CustomTabsSessionOptions? options,
}) async {
final session = await CustomTabsPlatform.instance.warmup(options);
return session as CustomTabsSession? ?? const CustomTabsSession(null);
}

Future<void> invalidateSession(PlatformSession session) {
return CustomTabsPlatform.instance.invalidate(session);
}
71 changes: 71 additions & 0 deletions flutter_custom_tabs/test/launcher_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,75 @@ void main() {
await closeCustomTabs();
expect(mock.closeAllIfPossibleCalled, isTrue);
});

test('warmupCustomTabs() invoke method "warmup" with options', () async {
const options = CustomTabsSessionOptions(
prefersDefaultBrowser: true,
);
const sessionPackageName = 'com.example.browser';
mock.setWarmupExpectations(
customTabsOptions: options,
customTabsSession: const CustomTabsSession(sessionPackageName),
);

final actualSession = await warmupCustomTabs(options: options);
expect(actualSession.packageName, sessionPackageName);
expect(mock.warmupCalled, isTrue);
});

test('warmupCustomTabs() invoke method "warmup" with no options', () async {
const sessionPackageName = 'com.example.browser';
mock.setWarmupExpectations(
customTabsSession: const CustomTabsSession(sessionPackageName),
);

final actualSession = await warmupCustomTabs();
expect(actualSession.packageName, sessionPackageName);
expect(mock.warmupCalled, isTrue);
});

test(
'warmupCustomTabs() returns empty CustomTabsSession when session is null',
() async {
mock.setWarmupExpectations(
customTabsSession: null,
);

final actualSession = await warmupCustomTabs();
expect(actualSession.packageName, isNull);
expect(mock.warmupCalled, isTrue);
});

test('invalidateSession() invoke method "invalidate" with CustomTabsSession',
() async {
const session = CustomTabsSession('com.example.browser');
mock.setInvalidateExpectations(session: session);

await invalidateSession(session);
expect(mock.invalidateCalled, isTrue);
});

test(
'invalidateSession() invoke method "invalidate" with SafariViewPrewarmingSession',
() async {
const session = SafariViewPrewarmingSession('test');
mock.setInvalidateExpectations(session: session);

await invalidateSession(session);
expect(mock.invalidateCalled, isTrue);
});

test(
'invalidateSession() invoke method "invalidate" with non-PlatformSession implementation',
() async {
const session = _Session();
mock.setInvalidateExpectations(session: session);

await invalidateSession(session);
expect(mock.invalidateCalled, isTrue);
});
}

class _Session implements PlatformSession {
const _Session();
}
45 changes: 44 additions & 1 deletion flutter_custom_tabs/test/mocks/mock_custom_tabs_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ class MockCustomTabsPlatform extends Fake
bool? prefersDeepLink;
CustomTabsOptions? customTabsOptions;
SafariViewControllerOptions? safariVCOptions;

PlatformOptions? sessionOptions;
PlatformSession? session;
bool launchUrlCalled = false;
bool closeAllIfPossibleCalled = false;
bool warmupCalled = false;
bool invalidateCalled = false;

void setLaunchExpectations({
required String url,
Expand All @@ -26,6 +29,20 @@ class MockCustomTabsPlatform extends Fake
this.safariVCOptions = safariVCOptions;
}

void setWarmupExpectations({
PlatformOptions? customTabsOptions,
PlatformSession? customTabsSession,
}) {
sessionOptions = customTabsOptions;
session = customTabsSession;
}

void setInvalidateExpectations({
required PlatformSession session,
}) {
this.session = session;
}

@override
Future<void> launch(
String? urlString, {
Expand Down Expand Up @@ -59,4 +76,30 @@ class MockCustomTabsPlatform extends Fake
Future<void> closeAllIfPossible() async {
closeAllIfPossibleCalled = true;
}

@override
Future<PlatformSession?> warmup([PlatformOptions? options]) async {
if (options is CustomTabsSessionOptions) {
final expected = sessionOptions as CustomTabsSessionOptions;
expect(options.prefersDefaultBrowser, expected.prefersDefaultBrowser);
} else {
expect(options, isNull);
}
warmupCalled = true;
return session;
}

@override
Future<void> invalidate(PlatformSession session) async {
if (session is CustomTabsSession) {
final expected = this.session as CustomTabsSession;
expect(session.packageName, expected.packageName);
} else if (session is SafariViewPrewarmingSession) {
final expected = this.session as SafariViewPrewarmingSession;
expect(session.id, expected.id);
} else {
expect(session, isNotNull);
}
invalidateCalled = true;
}
}
16 changes: 8 additions & 8 deletions flutter_custom_tabs_android/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ android {
namespace 'com.github.droibit.plugins.flutter.customtabs'
}

compileSdk 33
compileSdk 34

defaultConfig {
minSdk 19
Expand All @@ -48,14 +48,14 @@ android {
}

dependencies {
implementation 'androidx.browser:browser:1.5.0'
implementation 'com.github.droibit:customtabslauncher:2.0.0'
implementation 'androidx.browser:browser:1.8.0'
implementation 'com.github.droibit:customtabslauncher:101ff5b351'

testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation 'org.mockito:mockito-core:5.8.0'
testImplementation 'com.google.truth:truth:1.1.5'
testImplementation 'androidx.test.ext:truth:1.5.0'
testImplementation 'androidx.test.ext:junit:1.1.5'
testImplementation 'org.robolectric:robolectric:4.11'
testImplementation 'org.mockito:mockito-core:5.12.0'
testImplementation 'com.google.truth:truth:1.4.4'
testImplementation 'androidx.test.ext:truth:1.6.0'
testImplementation 'androidx.test.ext:junit:1.2.1'
}
}
Loading

0 comments on commit 87cf819

Please sign in to comment.