androidx 库中的 browser 库是什么
androidx.browser 库通过 Android Custom Tabs(自定义标签页) 帮助我们在用户默认浏览器中显示网页。例如,在新闻类应用中,当用户点击某条特定新闻的详情页以访问特定网站时,我们可以在应用内通过用户默认的浏览器程序来展示该网页。这个浏览器可能是 Chrome,或者是用户设置的任何其他默认浏览器。这种方法在利用浏览器功能和安全特性的同时,确保了更好的用户体验。
为什么选择 Android Custom Tabs?
在 Android 应用中展示网页是一个常见的需求。虽然许多应用仍在使用 WebView 在应用内展示网页,但这种做法存在几个明显的缺陷:
- 沉重的上下文切换与功能缺失:虽然直接调用外部浏览器可以提供完整的功能,但对于用户来说,这种从应用跳转到浏览器的切换感非常沉重。与此同时,WebView 虽然留在应用内,却不具备成熟浏览器的特性(如导航控制或地址栏),且无法像 Custom Tabs 那样进行深度定制。
- Compose 集成挑战 :除此之外,WebView 属于传统的 View 系统,因此无法直接集成到 Compose 项目中。虽然可以使用
AndroidView等互操作 API 来桥接,但这并非一种理想或优雅的解决方案。
在 Compose 项目中集成 在应用或模块的 build.gradle 文件中,为所需的构件(artifacts)添加依赖项:
arduino
dependencies {
implementation "androidx.browser:browser:1.8.0"
}
使用 CustomTabsIntent.Builder 创建一个 CustomTabsIntent,并通过调用 launchUrl() 并传递 Uri 来启动自定义标签页(Custom Tab):
kotlin
val url = "https://www.google.com"
val context = LocalContext.current
val intent = CustomTabsIntent.Builder()
.build()
Button(onClick = {
intent.launchUrl(context, Uri.parse(url))
}) {
Text(text = "Open Website")
}
定制外观
kotlin
val intent = CustomTabsIntent.Builder()
...
.setShowTitle(true)
.setStartAnimations(MainActivity.this, R.anim.slide_in_right, R.anim.slide_out_left)
.setExitAnimations(MainActivity.this, android.R.anim.slide_in_left, android.R.anim.slide_out_right)
.setUrlBarHidingEnabled(true)
.build();
-
.setShowTitle(true)--- 设置是否在自定义标签页中显示网页标题。 -
.setStartAnimations(...)和.setExitAnimations(...)--- 设置用户进入和离开网页时的进入与退出动画。 -
.setUrlBarHidingEnabled(true)--- 开启滚动时隐藏地址栏的功能,以便为用户提供更多浏览网页内容的自由空间。
这些仅仅是一些基础的定制选项。除此之外,你还可以实现更多高级功能,例如添加自定义菜单项 ,或者集成 Chrome 特有的功能(如远程调试、数据节省模式)等等。
预热
Custom Tabs的最大实际优势之一是浏览器可以在用户点击按钮之前准备好。
浏览器服务早期绑定。然后:
warmup(0L)在浏览器端预先启动工作CustomTabsSession被创建mayLaunchUrl()提示可能的导航目标
这意味着当登录开始时,浏览器不是冷启动。Google自己的文档说warmup()可以在打开链接时节省高达700ms。
这对认证特别相关,因为:
- 企业页面可能很沉重
- 重定向经常快速发生
- 用户感知延迟在第一次交互时最重要
WebView路径仍然可以调整,但它在应用进程内付出更多渲染和生命周期成本。
链接服务
kotlin
serviceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(
name: ComponentName,
client: CustomTabsClient
) {
customTabsClient = client
client.warmup(0L) // 预热浏览器引擎
customTabsSession = createSession(client) // 创建导航会话
}
override fun onServiceDisconnected(name: ComponentName?) {
customTabsClient = null
customTabsSession = null
}
}
CustomTabsClient.bindCustomTabsService(context, packageName, serviceConnection!!)
调用 warmup(0L) 在幕后让浏览器准备好 -- 它启动渲染引擎、启动JavaScript引擎、设置网络,并准备好显示网页所需的一切。虽然 0L 参数现在没什么用,但你仍然需要包含它。仅仅调用warmup就做了很多事情,即使你还没有要求任何特定的URL。
mayLaunchUrl
设置浏览器会话后,有 mayLaunchUrl()。这个让你给浏览器一个关于用户可能接下来打开哪个链接的提示。
kotlin
fun prepareUrl(url: Uri) {
if (customTabsSession == null) {
customTabsClient?.let {
customTabsSession = createSession(it)
}
}
customTabsSession?.mayLaunchUrl(url, null, null)
}
mayLaunchUrl() 的工作方式:它给浏览器一个提示,你可能很快会访问一个页面。所以,在幕后,浏览器开始准备 -- 它查找URL、打开连接,甚至可能提前抓取一些资源。所有这一切都在你点击那个链接之前发生。所以当你这样做时,页面几乎可以立即弹出。
这在Custom Tab启动之前被调用:
scss
performanceManager.prepareUrl(targetUri)
到 launcher.launch(customTabsIntent.intent) 启动时,浏览器已经代表用户做了大量设置工作。
单点登录实战
使用Android Custom Tabs来实现单点登录,比用WebView更合适。 当登录流程包含以下内容时,Custom Tabs特别出色:
- 共享浏览器Cookie
- Google/Microsoft/Okta/Auth0式登录
- 跨多个域的重定向
- 企业SSO
- 密码管理器自动填充
- 设备/浏览器信任假设
此时,浏览器不仅仅是渲染器,它成为安全模型的一部分。
- 浏览器处理安全关键的浏览器功能
- 减少应用中的安全责任
- 利用系统浏览器的安全更新
- 用户凭据从不通过应用进程
- 更少的耦合意味着更少的安全问题
1. 创建登录Url
kotlin
fun buildCustomTabsLoginUrl(url: String? = "10.0.2.2:3000/"): String {
val base = url // 模拟器localhost
return "${base}?callbackUrl=androidcustomtabs://login"
}
2. 打开Custom Tab
应用使用准备好的Custom Tabs会话启动页面:
kotlin
fun openWithCustomTabs(
keyboardController: SoftwareKeyboardController?,
context: Context,
url: String,
performanceManager: CustomTabsPerformanceManager
): String? {
val targetUri = buildCustomTabsLoginUrl(url).toUri()
if (!performanceManager.isSupported()) {
return "No Custom Tabs supporting browser found on this device."
}
performanceManager.prepareUrl(targetUri)
val customTabsIntent = CustomTabsIntent.Builder(performanceManager.getSession())
.setShowTitle(true)
.setUrlBarHidingEnabled(true)
.setShareState(CustomTabsIntent.SHARE_STATE_ON)
.setInstantAppsEnabled(false)
.build()
.apply {
intent.`package` = performanceManager.getPackageName()
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
intent.data = targetUri
}
performanceManager.markLaunchStarted()
customTabsIntent.launchUrl(context, targetUri)
keyboardController?.hide()
return null
}
3. 创建会话并监听事件
kotlin
private fun createSession(client: CustomTabsClient): CustomTabsSession? {
return client.newSession(object : CustomTabsCallback() {
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
val now = SystemClock.elapsedRealtime()
when (navigationEvent) {
NAVIGATION_STARTED -> {
if (launchStartTimeMs > 0L)
navStartedAtMs = now - launchStartTimeMs
}
TAB_SHOWN -> {
if (launchStartTimeMs > 0L && firstPaintAtMs == null)
firstPaintAtMs = now - launchStartTimeMs
}
NAVIGATION_FINISHED -> {
val visibleMs = firstPaintAtMs ?: (now - launchStartTimeMs)
val fullMs = now - launchStartTimeMs
context.toast(
"Custom Tabs metrics\n" +
"NAV ${navStartedAtMs}ms › paint ${visibleMs}ms › full ${fullMs}ms"
)
}
}
}
})
}
4. 用户输入登录详情
用户在登录页面上输入凭据。
5.### 成功登录后,Web通过深层链接回调重定向回Android
当登录成功且页面不在WebView内时,它重定向回应用。
实际重定向像这样构建:
kotlin
function redirectToApp(source) {
const callbackUrl = getCallbackUrl();
const target = new URL(callbackUrl);
target.searchParams.set('email', state.session.email);
target.searchParams.set('name', state.session.name);
target.searchParams.set('source', source || state.session.source || 'custom-tabs');
target.searchParams.set('transport', 'deep-link');
target.searchParams.set('token', state.session.jwt);
window.location.href = target.toString();
}
产生一个像这样的URI(带有有效令牌): androidcustomtabs://login?email=demo@sso.com&name=Demo%20User&source=login&transport=deep-link&token=...
5. Android接收并处理回调
xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="androidcustomtabs"
android:host="login" />
</intent-filter>
由于MainActivity设置为 launchMode="singleTask",当深层链接到达时,在现有活动实例上调用 onNewIntent 而不是创建新的:
kotlin
class MainActivity : ComponentActivity() {
var deepLinkIntent by mutableStateOf<Intent?>(null)
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
deepLinkIntent = intent
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
deepLinkIntent = intent
}
}
6. 恢复会话
BrowserDemoApp.kt监视该深层链接并恢复会话,因为 deepLinkIntent 是响应式 mutableStateOf 属性。BrowserDemoApp通过 LaunchedEffect 监视它:
kotlin
LaunchedEffect(activity?.deepLinkIntent) {
extractSessionFromUri(activity?.deepLinkIntent?.data)?.let {
currentSession = it
errorMessage = ""
context.toast("Custom Tabs returned to app for ${it.email}")
}
}
总结
在这篇文章中,我们探讨了 androidx.browser 库和 Android Custom Tabs 如何提供一种现代、安全且用户友好的 WebView 替代方案,用于在 Android 应用中展示网页内容。凭借与用户默认浏览器的无缝集成、增强的安全性以及丰富的定制选项,Custom Tabs 提供了一种高效的方式来增强应用功能,同时确保流畅的用户体验。