optimize webview

This commit is contained in:
2026-05-12 19:27:09 +03:30
parent fe17187df2
commit 9ec64fc179
15 changed files with 227 additions and 53 deletions
Generated
+1
View File
@@ -0,0 +1 @@
PSP
-4
View File
@@ -10,10 +10,6 @@
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="practice.PSP.tis">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="tis_app">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
+1
View File
@@ -43,6 +43,7 @@ android {
}
dependencies {
implementation(project(":design_system"))
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
+3 -1
View File
@@ -17,7 +17,9 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PSP">
android:theme="@style/Theme.PSP"
android:hardwareAccelerated="true"
>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -0,0 +1,9 @@
package com.example.psp.di
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object AppModule
@@ -2,6 +2,7 @@ package com.example.psp.screen
import android.app.Activity
import android.view.ViewGroup
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
@@ -12,18 +13,20 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.example.design_system.util.snackbar.SnackbarController
import com.example.design_system.util.snackbar.SnackbarEvent
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WebViewScreen(
url: String,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
var webView: WebView? by remember { mutableStateOf(null) }
val canGoBack = remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val context = LocalContext.current
var lastBackTime by remember { mutableLongStateOf(0L) }
@@ -37,7 +40,9 @@ fun WebViewScreen(
} else {
lastBackTime = now
scope.launch {
snackbarHostState.showSnackbar("Press back again to exit")
SnackbarController.sendEvent(
event = SnackbarEvent(message = "برای خروج یک بار دیگر بر روی دکمه‌ بازگشت کلیک کنید.")
)
}
}
}
@@ -62,7 +67,6 @@ fun WebViewScreen(
}
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
AndroidView(
modifier = Modifier
@@ -80,13 +84,29 @@ fun WebViewScreen(
canGoBack.value = view?.canGoBack() ?: false
}
}
settings.javaScriptEnabled = true
loadUrl(url)
overScrollMode = android.view.View.OVER_SCROLL_NEVER
// Apply comprehensive WebView settings
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
loadsImagesAutomatically = true
allowFileAccess = true
useWideViewPort = true
loadWithOverviewMode = true
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
}
webView = this
}
},
update = { view ->
webView = view
update = {
it.loadUrl(url)
}
)
}
@@ -0,0 +1,55 @@
package com.example.psp.viewmodel
import android.app.Application
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class WebViewViewModel @Inject constructor(
application: Application
) : AndroidViewModel(application) {
private var _preloadedWebView: WebView? = null
val preloadedWebView: WebView?
get() = _preloadedWebView
init {
preloadWebView()
}
private fun preloadWebView() {
viewModelScope.launch {
_preloadedWebView = WebView(getApplication()).apply {
overScrollMode = android.view.View.OVER_SCROLL_NEVER
webChromeClient = android.webkit.WebChromeClient()
// Apply the same settings as in WebViewScreen
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
loadsImagesAutomatically = true
allowFileAccess = true
useWideViewPort = true
loadWithOverviewMode = true
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
}
// Load the URL to preload
loadUrl("https://tis.shift-am.ir")
}
}
}
override fun onCleared() {
super.onCleared()
_preloadedWebView?.destroy()
_preloadedWebView = null
}
}
@@ -2,13 +2,14 @@ package com.example.design_system
import android.annotation.SuppressLint
import android.app.Activity
import android.view.View
import android.view.ViewGroup
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
@@ -21,8 +22,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.example.core.model.PrintEntity
import com.example.design_system.util.PspJavaScriptInterface
import com.example.design_system.util.WebViewManager
import com.example.design_system.util.snackbar.SnackbarController
import com.example.design_system.util.snackbar.SnackbarEvent
import kotlinx.coroutines.launch
@SuppressLint("JavascriptInterface")
@@ -30,9 +33,7 @@ import kotlinx.coroutines.launch
@Composable
fun PspWebView(
url: String,
webView: WebView?,
modifier: Modifier = Modifier,
onWebView: (WebView) -> Unit,
onPayClick: (amount: Long, id: String?) -> Unit,
onDeviceInfo: () -> String,
onPrintClick: (printEntities: List<PrintEntity>) -> Unit,
@@ -42,7 +43,8 @@ fun PspWebView(
val scope = rememberCoroutineScope()
val context = LocalContext.current
var lastBackTime by remember { mutableLongStateOf(0L) }
var isPaymentLaunching by remember { mutableStateOf(false) }
var webView by remember { mutableStateOf<WebView?>(null) }
// Use rememberUpdatedState to ensure the bridge always calls the latest callbacks
val currentOnPayClick by rememberUpdatedState(onPayClick)
@@ -69,7 +71,7 @@ fun PspWebView(
scope.launch {
SnackbarController.sendEvent(
SnackbarEvent(
message = "Press back again to exit"
message = "برای خروج، دوباره بر روی بازگشت کلیک کنید."
)
)
}
@@ -77,42 +79,36 @@ fun PspWebView(
}
}
DisposableEffect(Unit) {
webView = WebViewManager.getWebView(context)
onDispose {
webView?.let { WebViewManager.releaseWebView(it) }
}
}
AndroidView(
modifier = modifier,
factory = { ctx ->
WebView(ctx).apply {
(webView ?: WebViewManager.getWebView(ctx)).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
overScrollMode = View.OVER_SCROLL_NEVER
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
canGoBack.value = view?.canGoBack() ?: false
}
}
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
cacheMode = WebSettings.LOAD_DEFAULT
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
userAgentString =
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120.0 Mobile Safari/537.36"
}
addJavascriptInterface(bridge, "NativeBridge")
loadUrl(url)
onWebView(this)
}
},
update = { view ->
onWebView(view)
update = {
it.loadUrl(url)
}
)
@@ -0,0 +1,60 @@
package com.example.design_system.util
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import android.webkit.WebSettings
import android.webkit.WebView
import java.util.Stack
@SuppressLint("StaticFieldLeak")
object WebViewManager {
private const val POOL_SIZE = 1
private val webViewPool = Stack<WebView>()
fun preload(context: Context) {
if (webViewPool.isEmpty()) {
repeat(POOL_SIZE) {
val webView = createWebView(context)
webViewPool.push(webView)
}
}
}
fun getWebView(context: Context): WebView {
return if (webViewPool.isNotEmpty()) {
webViewPool.pop()
} else {
createWebView(context)
}
}
fun releaseWebView(webView: WebView) {
if (webViewPool.size < POOL_SIZE) {
webViewPool.push(webView)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun createWebView(context: Context): WebView {
return WebView(context).apply {
setLayerType(View.LAYER_TYPE_HARDWARE, null)
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
loadsImagesAutomatically = true
allowFileAccess = false
useWideViewPort = true
loadWithOverviewMode = true
mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
userAgentString = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120.0 Mobile Safari/537.36"
}
}
}
}
Vendored Regular → Executable
View File
+5 -1
View File
@@ -6,7 +6,11 @@ plugins {
android {
namespace = "com.example.p3"
compileSdk = 36
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
minSdk = 24
+1
View File
@@ -50,6 +50,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
+4 -1
View File
@@ -24,7 +24,10 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PSP"
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
>
<uses-library android:name="android.device" android:required="false" />
<activity
@@ -1,7 +1,13 @@
package com.example.tis_app
import android.app.Application
import com.example.design_system.util.WebViewManager
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class PspApplication : Application()
class PspApplication : Application() {
override fun onCreate() {
super.onCreate()
WebViewManager.preload(this)
}
}
@@ -7,6 +7,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@@ -15,9 +17,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.example.core.getAndroidId
import com.example.core.getDeviceName
import com.example.design_system.PspWebView
import com.example.design_system.util.WebViewManager
import com.example.tis_app.viewmodel.WebViewModel
private const val tisIp = "194.59.214.243:8090"
private const val tisWebUrl = "https://tis.shift-am.ir"
@Composable
fun TisWebViewScreen(
@@ -26,8 +29,19 @@ fun TisWebViewScreen(
) {
val context = LocalContext.current
var webView: WebView? by remember { mutableStateOf(null) }
var webView: WebView by remember { mutableStateOf(WebViewManager.getWebView(context)) }
var currentPaymentId by rememberSaveable { mutableStateOf<String?>(null) }
var isPaymentProcessing by rememberSaveable { mutableStateOf(false) }
var isPrinting by rememberSaveable { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val deviceInfo by remember {
mutableStateOf(
viewModel.deviceInfoJson(
androidId = getAndroidId(context),
getDeviceName()
)
)
}
// launcher to handle POS app result
val posLauncher = rememberLauncherForActivityResult(
@@ -38,34 +52,40 @@ fun TisWebViewScreen(
activityResult = result
)
webView?.post {
webView?.evaluateJavascript(
webView.post {
webView.evaluateJavascript(
viewModel.postPaymentResult(paymentResultEntity = paymentResultEntity),
null
)
}
isPaymentProcessing = false
}
PspWebView(
url = tisIp,
webView = webView,
url = tisWebUrl,
modifier = modifier,
onWebView = { updatedWebView -> webView = updatedWebView },
onPayClick = { amount: Long, id: String? ->
if (isPaymentProcessing) return@PspWebView
isPaymentProcessing = true
currentPaymentId = id
viewModel.pay(amount)
.also(posLauncher::launch)
},
onPrintClick = { printEntities ->
viewModel.print(printEntities)
if (isPrinting) return@PspWebView
isPrinting = true
coroutineScope.launch {
try {
viewModel.print(printEntities)
} finally {
isPrinting = false
}
}
},
onDeviceInfo = {
viewModel.deviceInfoJson(
androidId = getAndroidId(context),
getDeviceName()
)
deviceInfo
}
)
}