Closed tomastiminskas closed 10 months ago
@tomastiminskas Hi, thanks for your feedback! Can you please provide a minimal code example that can reproduce the issue?
@KevinnZou thanks for the quick response.
Here you can find the code I was trying: https://github.com/stakwork/sphinx-kotlin-ui/blob/tt/feature/js-bridge/common/src/desktopMain/kotlin/chat/sphinx/common/components/WebAppUI.kt
In the fun WebAppUI()
I'm initializing and showing the webview which loads without issues. If after a few seconds I run the code on fun AuthorizeViewUI()
in the same file, then the new window will show over the webview window and the webview will start to flicker as mentioned in the issue.
Let me know if you need any additional information, thanks in advance
@tomastiminskas Thanks for your information!
Are you using the Window
API from compose.desktop.components.splitPane
? Could you please just provide a simple code block that doesn't depend on other libraries and can directly reproduce the problem you are facing?
@KevinnZou Thanks again for the response. I will work on an example and provide a code block for you as soon as possible
@KevinnZou here is a sample of code to reproduce the issue
import androidx.compose.runtime.*
import androidx.compose.ui.window.application
import chat.sphinx.common.components.AuthorizeViewUI
import chat.sphinx.common.components.WebAppUI
import chat.sphinx.common.state.AuthorizeViewState
import chat.sphinx.common.viewmodel.WebAppViewModel
import dev.datlag.kcef.KCEF
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
fun main() = application {
var downloadProgress by remember { mutableStateOf(-1F) }
var initialized by remember { mutableStateOf(false) } // if true, KCEF can be used to create clients, browsers etc
val bundleLocation = System.getProperty("compose.application.resources.dir")?.let { File(it) } ?: File(".")
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) { // IO scope recommended but not required
KCEF.init(
builder = {
installDir(File(bundleLocation, "kcef-bundle")) // recommended, but not necessary
progress {
onDownloading {
downloadProgress = it
// use this if you want to display a download progress for example
}
onInitialized {
initialized = true
}
}
},
onError = {
println("Error ${it?.localizedMessage ?: ""}")
// error during initialization
},
onRestartRequired = {
// all required CEF packages downloaded but the application needs a restart to load them (unlikely to happen)
}
)
}
}
val webAppViewModel = remember { WebAppViewModel() }
WebAppUI(webAppViewModel)
val authorizeView by webAppViewModel.authorizeViewStateFlow.collectAsState()
(authorizeView as? AuthorizeViewState.Opened)?.let {
AuthorizeViewUI(webAppViewModel, it.budgetField)
}
}
package chat.sphinx.common.viewmodel
import chat.sphinx.common.state.AuthorizeViewState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class WebAppViewModel {
private val _webViewStateFlow: MutableStateFlow<String?> by lazy {
MutableStateFlow(null)
}
private val _authorizeViewStateFlow: MutableStateFlow<AuthorizeViewState> by lazy {
MutableStateFlow(AuthorizeViewState.Closed())
}
val webViewStateFlow: StateFlow<String?>
get() = _webViewStateFlow.asStateFlow()
val authorizeViewStateFlow: StateFlow<AuthorizeViewState>
get() = _authorizeViewStateFlow.asStateFlow()
fun toggleWebAppWindow(
url: String?
) {
CoroutineScope(SupervisorJob()).launch {
delay(1000L)
toggleWebViewWindow(url)
}
}
private fun toggleAuthorizeView() {
_webViewStateFlow?.value?.let { url ->
_authorizeViewStateFlow.value = AuthorizeViewState.Opened(url, false)
}
}
fun closeAuthorizeView() {
_authorizeViewStateFlow.value = AuthorizeViewState.Closed()
}
private fun toggleWebViewWindow(url: String?) {
url?.let { nnUrl ->
_webViewStateFlow.value = nnUrl
}
CoroutineScope(SupervisorJob()).launch {
openAuthorize()
}
}
private suspend fun openAuthorize() {
delay(60000L)
toggleAuthorizeView()
}
}
package chat.sphinx.common.components
import Roboto
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import chat.sphinx.common.viewmodel.WebAppViewModel
import chat.sphinx.utils.getPreferredWindowSize
import com.multiplatform.webview.web.WebView
import com.multiplatform.webview.web.WebViewState
import com.multiplatform.webview.web.rememberWebViewNavigator
import com.multiplatform.webview.web.rememberWebViewState
@Composable
fun WebAppUI(
webAppViewModel: WebAppViewModel
) {
var isOpen by remember { mutableStateOf(true) }
webAppViewModel.toggleWebAppWindow("https://google.com.ar")
if (isOpen) {
Window(
onCloseRequest = {
webAppViewModel.toggleWebAppWindow(null)
},
title = "Web App",
state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = getPreferredWindowSize(1200, 800)
)
) {
Box(
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Loading. Please wait...",
maxLines = 1,
fontSize = 14.sp,
fontFamily = Roboto,
modifier = Modifier.align(Alignment.Center)
)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
val webViewState by webAppViewModel.webViewStateFlow.collectAsState()
webViewState?.let { url ->
MaterialTheme {
val webViewState = rememberWebViewState(url)
val webViewNavigator = rememberWebViewNavigator()
initWebView(webViewState)
Column(Modifier.fillMaxSize()) {
WebView(
state = webViewState,
modifier = Modifier.fillMaxSize(),
navigator = webViewNavigator
)
}
}
}
}
}
}
}
}
@Composable
fun AuthorizeViewUI(
webAppViewModel: WebAppViewModel,
budgetField: Boolean
) {
var isOpen by remember { mutableStateOf(true) }
if (isOpen) {
Window(
onCloseRequest = { webAppViewModel.closeAuthorizeView() },
title = if (budgetField) "Set Budget" else "Authorize",
state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = getPreferredWindowSize(200, 200)
),
alwaysOnTop = false,
resizable = true,
focusable = true,
) {
// Box(
// modifier = Modifier.fillMaxSize()
// .background(androidx.compose.material3.MaterialTheme.colorScheme.background),
// ) {
//
// }
}
}
}
fun initWebView(webViewState: WebViewState) {
webViewState.webSettings.apply {
zoomLevel = 1.0
isJavaScriptEnabled = true
customUserAgentString = "Sphinx"
androidWebSettings.apply {
isAlgorithmicDarkeningAllowed = true
safeBrowsingEnabled = true
allowFileAccess = true
}
}
}
In this example I'm loading google webpage in a webview inside the main window of the app. The issue only happens when the second window opens after the web page finished loading, so I added a 60 second delay before showing the second Window (since the library usually takes around 50 seconds to load after launch). You can modify that value if needed. You will see that google page loads successfully and when the second window is presented over the main window it starts to flicker and google page content becomes invisible.
Let me know if this example works or you need something else from me
Thanks in advance
@tomastiminskas Thank you for the information. The code you shared is too complex for us to reproduce. However, I have written a simplified version and tested it locally. I have discovered an issue with the implementation of the desktop side, specifically a problem with backward writing which results in infinite recomposition. I will fix this issue in the next version. It is important to note that this issue only occurs if a recompose is triggered after the webview has loaded. This is why it works well in normal cases.
However, the backward write issue should not be the cause of your problem. Displaying an authentication window should not trigger the WebView window to recompose. Even if the backward write issue is resolved, the page will still refresh. The test code below opens a new window without recomposing the original window. Therefore, there must be an issue with your structure that is causing the unnecessary recomposition of the WebView window. If you can identify and fix this issue, the flickering effect will disappear without having to wait for the new version.
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
var restartRequired by remember { mutableStateOf(false) }
var downloading by remember { mutableStateOf(0F) }
var initialized by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
KCEF.init(builder = {
installDir(File("kcef-bundle"))
progress {
onDownloading {
downloading = it
}
onInitialized {
initialized = true
}
}
settings {
cachePath = File("cache").absolutePath
}
}, onError = {
it?.printStackTrace()
}, onRestartRequired = {
restartRequired = true
})
}
}
if (restartRequired) {
Text(text = "Restart required.")
} else {
if (initialized) {
WindowTestView()
} else {
Text(text = "Downloading $downloading%")
}
}
DisposableEffect(Unit) {
onDispose {
KCEF.disposeBlocking()
}
}
}
}
@Composable
fun WindowTestView() {
var showAuth by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO){
delay(5000)
showAuth = true
}
}
WebViewWindow()
if (showAuth) {
AuthWindow()
}
}
@Composable
fun WebViewWindow() {
println("WebViewWindow recompose")
Window(
onCloseRequest = { },
title = "WebView",
state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = DpSize(1200.dp, 800.dp)
),
alwaysOnTop = false,
resizable = true,
focusable = true,
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
) {
SimpleWebView()
}
}
}
@Composable
fun SimpleWebView() {
val state = rememberWebViewState("https://kotlinlang.org/")
state.webSettings.logSeverity = KLogSeverity.Debug
WebView(state = state, modifier = Modifier.fillMaxSize())
}
@Composable
fun AuthWindow() {
var isOpen by remember { mutableStateOf(true) }
if (isOpen) {
Window(
onCloseRequest = { isOpen = false },
title = "Authorize",
state = WindowState(
position = WindowPosition.Aligned(Alignment.Center)
),
alwaysOnTop = false,
resizable = true,
focusable = true,
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue),
) {
}
}
}
}
@KevinnZou thank you so much. I was able to fix it avoiding the recomposition of the webview.
I was able to load a url in the webview (inside a Window) and implement js bridge using the SNAPSHOT version. Now I'm facing a different issue. When I got a message from the web app I need to show a popup view with a button for the user to authorize some actions.
I first tried showing the AUTHORIZE view on top of the webview in the same window, and then I tried showing it in a new window. On both cases the webview starts to flicker as shown in the video.
Any ideas what might be causing this?
https://github.com/KevinnZou/compose-webview-multiplatform/assets/2242281/8bc5d2ad-3915-449d-83e0-f11ed30f119b