mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 01:17:05 +05:30
feat(web): init
This commit is contained in:
parent
6084e5bb30
commit
5bcfe0a85c
13 changed files with 685 additions and 7 deletions
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
<issue
|
||||
id="VectorPath"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
||||
|
|
|
@ -11,6 +11,7 @@ junit = "5.10.1"
|
|||
konvert = "2.4.0"
|
||||
kotlin = "1.9.21"
|
||||
kotlinResult = "1.1.18"
|
||||
lifecycle = "2.7.0-rc01"
|
||||
retrofit = "2.9.0"
|
||||
richtext = "0.17.0"
|
||||
sentry-sdk = "7.0.0"
|
||||
|
@ -36,7 +37,10 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
|||
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" }
|
||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
|
||||
androidx-core-splashscreen = "androidx.core:core-splashscreen:1.0.1"
|
||||
androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"
|
||||
androidx-navigation-compose = "androidx.navigation:navigation-compose:2.7.5"
|
||||
androidx-paging-compose = "androidx.paging:paging-compose:3.2.1"
|
||||
|
@ -73,6 +77,7 @@ junit-legacy = "junit:junit:4.13.2"
|
|||
konvert-api = { module = "io.mcarle:konvert-api", version.ref = "konvert" }
|
||||
konvert-processor = { module = "io.mcarle:konvert", version.ref = "konvert" }
|
||||
kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6"
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
||||
|
|
|
@ -163,4 +163,5 @@ include(
|
|||
"database:core",
|
||||
"database:impl",
|
||||
"model",
|
||||
"web",
|
||||
)
|
||||
|
|
29
web/build.gradle.kts
Normal file
29
web/build.gradle.kts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.android-library")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.accompanist.web"
|
||||
buildFeatures.compose = true
|
||||
composeOptions.kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
kotlin.explicitApi()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui.util)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
}
|
4
web/lint-baseline.xml
Normal file
4
web/lint-baseline.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
7
web/src/main/AndroidManifest.xml
Normal file
7
web/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<manifest />
|
632
web/src/main/java/com/google/accompanist/web/WebView.kt
Normal file
632
web/src/main/java/com/google/accompanist/web/WebView.kt
Normal file
|
@ -0,0 +1,632 @@
|
|||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package com.google.accompanist.web
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.google.accompanist.web.LoadingState.Finished
|
||||
import com.google.accompanist.web.LoadingState.Loading
|
||||
import dev.msfjarvis.claw.core.coroutines.DefaultDispatcherProvider
|
||||
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* A wrapper around the Android View WebView to provide a basic WebView composable.
|
||||
*
|
||||
* If you require more customisation you are most likely better rolling your own and using this
|
||||
* wrapper as an example.
|
||||
*
|
||||
* The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it is
|
||||
* incorrectly sizing, use the layoutParams composable function instead.
|
||||
*
|
||||
* @param state The webview state holder where the Uri to load is defined.
|
||||
* @param modifier A compose modifier
|
||||
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
||||
* the WebView back.
|
||||
* @param navigator An optional navigator object that can be used to control the WebView's
|
||||
* navigation from outside the composable.
|
||||
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
||||
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will
|
||||
* be subsequently overwritten after this lambda is called.
|
||||
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved if
|
||||
* you need to save and restore state in this WebView.
|
||||
* @param client Provides access to WebViewClient via subclassing
|
||||
* @param chromeClient Provides access to WebChromeClient via subclassing
|
||||
* @param factory An optional WebView factory for using a custom subclass of WebView
|
||||
*/
|
||||
@Composable
|
||||
public fun WebView(
|
||||
state: WebViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
captureBackPresses: Boolean = true,
|
||||
navigator: WebViewNavigator = rememberWebViewNavigator(),
|
||||
onCreated: (WebView) -> Unit = {},
|
||||
onDispose: (WebView) -> Unit = {},
|
||||
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
|
||||
chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
|
||||
factory: ((Context) -> WebView)? = null,
|
||||
) {
|
||||
BoxWithConstraints(modifier) {
|
||||
// WebView changes it's layout strategy based on
|
||||
// it's layoutParams. We convert from Compose Modifier to
|
||||
// layout params here.
|
||||
val width =
|
||||
if (constraints.hasFixedWidth) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
||||
val height =
|
||||
if (constraints.hasFixedHeight) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
||||
|
||||
val layoutParams = FrameLayout.LayoutParams(width, height)
|
||||
|
||||
WebView(
|
||||
state,
|
||||
layoutParams,
|
||||
Modifier,
|
||||
captureBackPresses,
|
||||
navigator,
|
||||
onCreated,
|
||||
onDispose,
|
||||
client,
|
||||
chromeClient,
|
||||
factory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around the Android View WebView to provide a basic WebView composable.
|
||||
*
|
||||
* If you require more customisation you are most likely better rolling your own and using this
|
||||
* wrapper as an example.
|
||||
*
|
||||
* The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it is
|
||||
* incorrectly sizing, use the layoutParams composable function instead.
|
||||
*
|
||||
* @param state The webview state holder where the Uri to load is defined.
|
||||
* @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView.
|
||||
* @param modifier A compose modifier
|
||||
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
||||
* the WebView back.
|
||||
* @param navigator An optional navigator object that can be used to control the WebView's
|
||||
* navigation from outside the composable.
|
||||
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
||||
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will
|
||||
* be subsequently overwritten after this lambda is called.
|
||||
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved if
|
||||
* you need to save and restore state in this WebView.
|
||||
* @param client Provides access to WebViewClient via subclassing
|
||||
* @param chromeClient Provides access to WebChromeClient via subclassing
|
||||
* @param factory An optional WebView factory for using a custom subclass of WebView
|
||||
*/
|
||||
@Composable
|
||||
public fun WebView(
|
||||
state: WebViewState,
|
||||
layoutParams: FrameLayout.LayoutParams,
|
||||
modifier: Modifier = Modifier,
|
||||
captureBackPresses: Boolean = true,
|
||||
navigator: WebViewNavigator = rememberWebViewNavigator(),
|
||||
onCreated: (WebView) -> Unit = {},
|
||||
onDispose: (WebView) -> Unit = {},
|
||||
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
|
||||
chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
|
||||
factory: ((Context) -> WebView)? = null,
|
||||
) {
|
||||
val webView = state.webView
|
||||
|
||||
BackHandler(captureBackPresses && navigator.canGoBack) { webView?.goBack() }
|
||||
|
||||
webView?.let { wv ->
|
||||
LaunchedEffect(wv, navigator) { with(navigator) { wv.handleNavigationEvents() } }
|
||||
|
||||
LaunchedEffect(wv, state) {
|
||||
snapshotFlow { state.content }
|
||||
.collect { content ->
|
||||
when (content) {
|
||||
is WebContent.Url -> {
|
||||
wv.loadUrl(content.url, content.additionalHttpHeaders)
|
||||
}
|
||||
is WebContent.Data -> {
|
||||
wv.loadDataWithBaseURL(
|
||||
content.baseUrl,
|
||||
content.data,
|
||||
content.mimeType,
|
||||
content.encoding,
|
||||
content.historyUrl
|
||||
)
|
||||
}
|
||||
is WebContent.Post -> {
|
||||
wv.postUrl(content.url, content.postData)
|
||||
}
|
||||
is WebContent.NavigatorOnly -> {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the state of the client and chrome client
|
||||
// This is done internally to ensure they always are the same instance as the
|
||||
// parent Web composable
|
||||
client.state = state
|
||||
client.navigator = navigator
|
||||
chromeClient.state = state
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
(factory?.invoke(context) ?: WebView(context))
|
||||
.apply {
|
||||
onCreated(this)
|
||||
|
||||
this.layoutParams = layoutParams
|
||||
|
||||
state.viewState?.let { this.restoreState(it) }
|
||||
|
||||
webChromeClient = chromeClient
|
||||
webViewClient = client
|
||||
}
|
||||
.also { state.webView = it }
|
||||
},
|
||||
modifier = modifier,
|
||||
onRelease = { onDispose(it) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* AccompanistWebViewClient
|
||||
*
|
||||
* A parent class implementation of WebViewClient that can be subclassed to add custom behaviour.
|
||||
*
|
||||
* As Accompanist Web needs to set its own web client to function, it provides this intermediary
|
||||
* class that can be overriden if further custom behaviour is required.
|
||||
*/
|
||||
public open class AccompanistWebViewClient : WebViewClient() {
|
||||
public open lateinit var state: WebViewState
|
||||
internal set
|
||||
|
||||
public open lateinit var navigator: WebViewNavigator
|
||||
internal set
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
state.loadingState = Loading(0.0f)
|
||||
state.errorsForCurrentRequest.clear()
|
||||
state.pageTitle = null
|
||||
state.pageIcon = null
|
||||
|
||||
state.lastLoadedUrl = url
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
state.loadingState = Finished
|
||||
}
|
||||
|
||||
override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) {
|
||||
super.doUpdateVisitedHistory(view, url, isReload)
|
||||
|
||||
navigator.canGoBack = view.canGoBack()
|
||||
navigator.canGoForward = view.canGoForward()
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?
|
||||
) {
|
||||
super.onReceivedError(view, request, error)
|
||||
|
||||
if (error != null) {
|
||||
state.errorsForCurrentRequest.add(WebViewError(request, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AccompanistWebChromeClient
|
||||
*
|
||||
* A parent class implementation of WebChromeClient that can be subclassed to add custom behaviour.
|
||||
*
|
||||
* As Accompanist Web needs to set its own web client to function, it provides this intermediary
|
||||
* class that can be overridden if further custom behaviour is required.
|
||||
*/
|
||||
public open class AccompanistWebChromeClient : WebChromeClient() {
|
||||
public open lateinit var state: WebViewState
|
||||
internal set
|
||||
|
||||
override fun onReceivedTitle(view: WebView, title: String?) {
|
||||
super.onReceivedTitle(view, title)
|
||||
state.pageTitle = title
|
||||
}
|
||||
|
||||
override fun onReceivedIcon(view: WebView, icon: Bitmap?) {
|
||||
super.onReceivedIcon(view, icon)
|
||||
state.pageIcon = icon
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
if (state.loadingState is Finished) return
|
||||
state.loadingState = Loading(newProgress / 100.0f)
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WebContent {
|
||||
public data class Url(
|
||||
val url: String,
|
||||
val additionalHttpHeaders: Map<String, String> = emptyMap(),
|
||||
) : WebContent()
|
||||
|
||||
public data class Data(
|
||||
val data: String,
|
||||
val baseUrl: String? = null,
|
||||
val encoding: String = "utf-8",
|
||||
val mimeType: String? = null,
|
||||
val historyUrl: String? = null
|
||||
) : WebContent()
|
||||
|
||||
public data class Post(val url: String, val postData: ByteArray) : WebContent() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Post
|
||||
|
||||
if (url != other.url) return false
|
||||
if (!postData.contentEquals(other.postData)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = url.hashCode()
|
||||
result = 31 * result + postData.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public data object NavigatorOnly : WebContent()
|
||||
}
|
||||
|
||||
internal fun WebContent.withUrl(url: String) =
|
||||
when (this) {
|
||||
is WebContent.Url -> copy(url = url)
|
||||
else -> WebContent.Url(url)
|
||||
}
|
||||
|
||||
/** Sealed class for constraining possible loading states. See [Loading] and [Finished]. */
|
||||
public sealed class LoadingState {
|
||||
/** Describes a WebView that has not yet loaded for the first time. */
|
||||
public data object Initializing : LoadingState()
|
||||
|
||||
/**
|
||||
* Describes a webview between `onPageStarted` and `onPageFinished` events, contains a [progress]
|
||||
* property which is updated by the webview.
|
||||
*/
|
||||
public data class Loading(val progress: Float) : LoadingState()
|
||||
|
||||
/** Describes a webview that has finished loading content. */
|
||||
public data object Finished : LoadingState()
|
||||
}
|
||||
|
||||
/**
|
||||
* A state holder to hold the state for the WebView. In most cases this will be remembered using the
|
||||
* rememberWebViewState(uri) function.
|
||||
*/
|
||||
@Stable
|
||||
public class WebViewState(webContent: WebContent) {
|
||||
public var lastLoadedUrl: String? by mutableStateOf(null)
|
||||
internal set
|
||||
|
||||
/** The content being loaded by the WebView */
|
||||
public var content: WebContent by mutableStateOf(webContent)
|
||||
|
||||
/**
|
||||
* Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with
|
||||
* progress) or the data loading has [LoadingState.Finished]. See [LoadingState]
|
||||
*/
|
||||
public var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)
|
||||
internal set
|
||||
|
||||
/** Whether the webview is currently loading data in its main frame */
|
||||
public val isLoading: Boolean
|
||||
get() = loadingState !is Finished
|
||||
|
||||
/** The title received from the loaded content of the current page */
|
||||
public var pageTitle: String? by mutableStateOf(null)
|
||||
internal set
|
||||
|
||||
/** the favicon received from the loaded content of the current page */
|
||||
public var pageIcon: Bitmap? by mutableStateOf(null)
|
||||
internal set
|
||||
|
||||
/**
|
||||
* A list for errors captured in the last load. Reset when a new page is loaded. Errors could be
|
||||
* from any resource (iframe, image, etc.), not just for the main page. For more fine grained
|
||||
* control use the OnError callback of the WebView.
|
||||
*/
|
||||
public val errorsForCurrentRequest: SnapshotStateList<WebViewError> = mutableStateListOf()
|
||||
|
||||
/**
|
||||
* The saved view state from when the view was destroyed last. To restore state, use the navigator
|
||||
* and only call loadUrl if the bundle is null. See WebViewSaveStateSample.
|
||||
*/
|
||||
public var viewState: Bundle? = null
|
||||
internal set
|
||||
|
||||
// We need access to this in the state saver. An internal DisposableEffect or AndroidView
|
||||
// onDestroy is called after the state saver and so can't be used.
|
||||
internal var webView by mutableStateOf<WebView?>(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows control over the navigation of a WebView from outside the composable. E.g. for performing
|
||||
* a back navigation in response to the user clicking the "up" button in a TopAppBar.
|
||||
*
|
||||
* @see [rememberWebViewNavigator]
|
||||
*/
|
||||
@Stable
|
||||
public class WebViewNavigator(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
) {
|
||||
private sealed interface NavigationEvent {
|
||||
data object Back : NavigationEvent
|
||||
|
||||
data object Forward : NavigationEvent
|
||||
|
||||
data object Reload : NavigationEvent
|
||||
|
||||
data object StopLoading : NavigationEvent
|
||||
|
||||
data class LoadUrl(
|
||||
val url: String,
|
||||
val additionalHttpHeaders: Map<String, String> = emptyMap()
|
||||
) : NavigationEvent
|
||||
|
||||
data class LoadHtml(
|
||||
val html: String,
|
||||
val baseUrl: String? = null,
|
||||
val mimeType: String? = null,
|
||||
val encoding: String? = "utf-8",
|
||||
val historyUrl: String? = null
|
||||
) : NavigationEvent
|
||||
|
||||
data class PostUrl(val url: String, val postData: ByteArray) : NavigationEvent {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PostUrl
|
||||
|
||||
if (url != other.url) return false
|
||||
if (!postData.contentEquals(other.postData)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = url.hashCode()
|
||||
result = 31 * result + postData.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val navigationEvents: MutableSharedFlow<NavigationEvent> = MutableSharedFlow(replay = 1)
|
||||
|
||||
// Use Dispatchers.Main to ensure that the webview methods are called on UI thread
|
||||
internal suspend fun WebView.handleNavigationEvents(): Nothing =
|
||||
withContext(dispatcherProvider.main()) {
|
||||
navigationEvents.collect { event ->
|
||||
when (event) {
|
||||
is NavigationEvent.Back -> goBack()
|
||||
is NavigationEvent.Forward -> goForward()
|
||||
is NavigationEvent.Reload -> reload()
|
||||
is NavigationEvent.StopLoading -> stopLoading()
|
||||
is NavigationEvent.LoadHtml ->
|
||||
loadDataWithBaseURL(
|
||||
event.baseUrl,
|
||||
event.html,
|
||||
event.mimeType,
|
||||
event.encoding,
|
||||
event.historyUrl
|
||||
)
|
||||
is NavigationEvent.LoadUrl -> {
|
||||
loadUrl(event.url, event.additionalHttpHeaders)
|
||||
}
|
||||
is NavigationEvent.PostUrl -> {
|
||||
postUrl(event.url, event.postData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** True when the web view is able to navigate backwards, false otherwise. */
|
||||
public var canGoBack: Boolean by mutableStateOf(false)
|
||||
internal set
|
||||
|
||||
/** True when the web view is able to navigate forwards, false otherwise. */
|
||||
public var canGoForward: Boolean by mutableStateOf(false)
|
||||
internal set
|
||||
|
||||
public fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()) {
|
||||
coroutineScope.launch {
|
||||
navigationEvents.emit(NavigationEvent.LoadUrl(url, additionalHttpHeaders))
|
||||
}
|
||||
}
|
||||
|
||||
public fun loadHtml(
|
||||
html: String,
|
||||
baseUrl: String? = null,
|
||||
mimeType: String? = null,
|
||||
encoding: String? = "utf-8",
|
||||
historyUrl: String? = null
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
navigationEvents.emit(NavigationEvent.LoadHtml(html, baseUrl, mimeType, encoding, historyUrl))
|
||||
}
|
||||
}
|
||||
|
||||
public fun postUrl(url: String, postData: ByteArray) {
|
||||
coroutineScope.launch { navigationEvents.emit(NavigationEvent.PostUrl(url, postData)) }
|
||||
}
|
||||
|
||||
/** Navigates the webview back to the previous page. */
|
||||
public fun navigateBack() {
|
||||
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) }
|
||||
}
|
||||
|
||||
/** Navigates the webview forward after going back from a page. */
|
||||
public fun navigateForward() {
|
||||
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) }
|
||||
}
|
||||
|
||||
/** Reloads the current page in the webview. */
|
||||
public fun reload() {
|
||||
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) }
|
||||
}
|
||||
|
||||
/** Stops the current page load (if one is loading). */
|
||||
public fun stopLoading() {
|
||||
coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided
|
||||
* override.
|
||||
*/
|
||||
@Composable
|
||||
public fun rememberWebViewNavigator(
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(),
|
||||
): WebViewNavigator =
|
||||
remember(coroutineScope) { WebViewNavigator(coroutineScope, dispatcherProvider) }
|
||||
|
||||
/** A wrapper class to hold errors from the WebView. */
|
||||
@Immutable
|
||||
public data class WebViewError(
|
||||
/** The request the error came from. */
|
||||
val request: WebResourceRequest?,
|
||||
/** The error that was reported. */
|
||||
val error: WebResourceError
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a WebView state that is remembered across Compositions.
|
||||
*
|
||||
* @param url The url to load in the WebView
|
||||
* @param additionalHttpHeaders Optional, additional HTTP headers that are passed to
|
||||
* [WebView.loadUrl]. Note that these headers are used for all subsequent requests of the WebView.
|
||||
*/
|
||||
@Composable
|
||||
public fun rememberWebViewState(
|
||||
url: String,
|
||||
additionalHttpHeaders: ImmutableMap<String, String> = persistentMapOf()
|
||||
): WebViewState =
|
||||
// Rather than using .apply {} here we will recreate the state, this prevents
|
||||
// a recomposition loop when the webview updates the url itself.
|
||||
remember {
|
||||
WebViewState(WebContent.Url(url = url, additionalHttpHeaders = additionalHttpHeaders))
|
||||
}
|
||||
.apply {
|
||||
this.content = WebContent.Url(url = url, additionalHttpHeaders = additionalHttpHeaders)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WebView state that is remembered across Compositions.
|
||||
*
|
||||
* @param data The uri to load in the WebView
|
||||
*/
|
||||
@Composable
|
||||
public fun rememberWebViewStateWithHTMLData(
|
||||
data: String,
|
||||
baseUrl: String? = null,
|
||||
encoding: String = "utf-8",
|
||||
mimeType: String? = null,
|
||||
historyUrl: String? = null
|
||||
): WebViewState =
|
||||
remember { WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl)) }
|
||||
.apply { this.content = WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl) }
|
||||
|
||||
/**
|
||||
* Creates a WebView state that is remembered across Compositions.
|
||||
*
|
||||
* @param url The url to load in the WebView
|
||||
* @param postData The data to be posted to the WebView with the url
|
||||
*/
|
||||
@Composable
|
||||
public fun rememberWebViewState(url: String, postData: ByteArray): WebViewState =
|
||||
// Rather than using .apply {} here we will recreate the state, this prevents
|
||||
// a recomposition loop when the webview updates the url itself.
|
||||
remember { WebViewState(WebContent.Post(url = url, postData = postData)) }
|
||||
.apply { this.content = WebContent.Post(url = url, postData = postData) }
|
||||
|
||||
/**
|
||||
* Creates a WebView state that is remembered across Compositions and saved across activity
|
||||
* recreation. When using saved state, you cannot change the URL via recomposition. The only way to
|
||||
* load a URL is via a WebViewNavigator.
|
||||
*/
|
||||
@Composable
|
||||
public fun rememberSaveableWebViewState(): WebViewState =
|
||||
rememberSaveable(saver = WebStateSaver) { WebViewState(WebContent.NavigatorOnly) }
|
||||
|
||||
public val WebStateSaver: Saver<WebViewState, Any> = run {
|
||||
val pageTitleKey = "pagetitle"
|
||||
val lastLoadedUrlKey = "lastloaded"
|
||||
val stateBundle = "bundle"
|
||||
|
||||
mapSaver(
|
||||
save = {
|
||||
val viewState = Bundle().apply { it.webView?.saveState(this) }
|
||||
mapOf(
|
||||
pageTitleKey to it.pageTitle,
|
||||
lastLoadedUrlKey to it.lastLoadedUrl,
|
||||
stateBundle to viewState
|
||||
)
|
||||
},
|
||||
restore = {
|
||||
WebViewState(WebContent.NavigatorOnly).apply {
|
||||
this.pageTitle = it[pageTitleKey] as String?
|
||||
this.lastLoadedUrl = it[lastLoadedUrlKey] as String?
|
||||
this.viewState = it[stateBundle] as Bundle?
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue