turns-00042.parquet:36452
7c5ce70f3f277f909fdab006
turn 1/2gpt-4o-mini-2024-07-18EnglishRussia1545 words
degenerate_repetitionAbsentFinal dense release
USER
Существует проблема в WebView, если мы пройдём куда-то вглубь сайта и свернём и развернём приложение, то вызовется onViewStateChange - LoadableData.Success и при вызове LoadPage(viewState.value) перезагрузит страницу на начальную, на которую был переход в приложении, как это исправить? package ru.maxi.mobile.feature.webfeature.view
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.webkit.CookieManager
import android.webkit.DownloadListener
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.view.MenuProvider
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.firebase.Firebase
import com.google.firebase.crashlytics.crashlytics
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import ru.maxi.mobile.BuildConfig
import ru.maxi.mobile.R
import ru.maxi.mobile.base.ui.MvvmBaseFragment
import ru.maxi.mobile.base.ui.manager.showConfirmDialog
import ru.maxi.mobile.core.data.LoadableData
import ru.maxi.mobile.core.navigation.parentRouter
import ru.maxi.mobile.databinding.FragmentWebviewBinding
import ru.maxi.mobile.feature.webfeature.view.model.WebFeaturePage
import ru.maxi.mobile.feature.webfeature.view.model.deepLink.WebCloseDialogData
import ru.maxi.mobile.presentation.common.NegativeButton
import ru.maxi.mobile.presentation.common.PositiveButton
import ru.maxi.mobile.presentation.common.screens.CommonScreens
import ru.maxi.mobile.ui.common.BackButtonListener
import ru.maxi.mobile.uikit.utils.lifecycleAwareLazy
import java.io.File
/**
* Экран Web-фичи
*/
@AndroidEntryPoint
class WebFeatureFragment : MvvmBaseFragment(R.layout.fragment_webview), BackButtonListener {
private val viewBinding by viewBinding(FragmentWebviewBinding::bind)
private val viewModel by viewModels<WebFeatureViewModel>()
private val menuProvider by lifecycleAwareLazy {
WebFeatureMenuProvider()
}
override val analyticsName: String = "_WebFeature"
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner, Lifecycle.State.STARTED)
setTitle("")
bottomBarManager?.isVisible = false
with(viewBinding) {
webView.apply {
clearAppCaches()
settings.apply {
domStorageEnabled = true
cacheMode = WebSettings.LOAD_NO_CACHE
textZoom = 100
javaScriptEnabled = true
}
addJavascriptInterface(viewModel, "MaxiApp")
webViewClient = CustomWebViewClient()
setDownloadListener(CustomDownloadListener())
}
noConnectionView.setOnActionButtonClickListener { webView.reload() }
}
with(viewLifecycleOwner) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.viewState.collect(::onViewStateChange) }
launch {
viewModel.shouldCloseScreen
.filter { it }
.collect { parentRouter.exit() }
}
}
}
}
}
override fun onStop() {
super.onStop()
CookieManager.getInstance().flush()
}
override fun onDestroyView() {
bottomBarManager?.isVisible = true
super.onDestroyView()
}
override fun onBackPressed(): Boolean {
if (!viewBinding.webView.canGoBack()) {
val closeDialogData = viewModel.closeDialogData ?: return false
showConfirmDialog(closeDialogData)
return true
}
viewBinding.webView.goBack()
return true
}
private fun WebView.clearAppCaches() {
try {
val webViewCacheDirs = listOf(
File("${context.applicationInfo.dataDir}/app_webview"),
File("${context.applicationInfo.dataDir}/app_hws_webview"),
)
webViewCacheDirs.forEach { webViewCacheDir ->
val filesList = webViewCacheDir.listFiles()
val folderToKeep = listOf("Default")
val filesToKeep = listOf("Cookies", "Cookies-journal")
filesList?.forEach {
if (!folderToKeep.contains(it.name)) {
it.deleteRecursively()
return@forEach
}
val filesInFolder = it.listFiles()
filesInFolder?.forEach { files ->
if (!filesToKeep.contains(files.name)) {
files.deleteRecursively()
}
}
}
}
} catch (e: Exception) {
Firebase.crashlytics.recordException(e)
}
}
private fun showConfirmDialog(dialogData: WebCloseDialogData) {
dialogManager.showConfirmDialog(
dialogData.text,
PositiveButton(dialogData.btnConfirm) {
dialogData.btnConfirmAction
?.let(::evaluateCloseDialogAction)
?: viewModel.closeScreen()
},
NegativeButton(dialogData.btnCancel),
)
}
private fun evaluateCloseDialogAction(javascriptAction: String) {
blockingLoaderManager.isShowing = true
viewBinding.webView.evaluateJavascript("(function() { $javascriptAction })();") {}
}
private fun setTitle(title: String) {
actionBarManager.setTitle(title)
actionBarManager.showBackButton()
}
private fun onViewStateChange(viewState: LoadableData<WebFeaturePage>) {
when (viewState) {
is LoadableData.Error -> with(viewBinding) {
webView.isInvisible = true
noConnectionView.isVisible = true
loaderView.hide()
}
is LoadableData.Loading -> with(viewBinding) {
val isInitialPage = webView.url == null
webView.isInvisible = isInitialPage
Log.d("TESTING", "onViewStateChange - LOADING isInitialPage = $isInitialPage")
noConnectionView.isGone = true
if (isInitialPage) {
loaderView.show()
} else {
loaderView.hide()
}
viewState.value?.let(::loadPage)
}
is LoadableData.Success -> with(viewBinding) {
Log.d("TESTING", "onViewStateChange - SUCCESS")
webView.isVisible = true
noConnectionView.isGone = true
loaderView.hide()
loadPage(viewState.value)
}
}
}
private fun loadPage(page: WebFeaturePage) {
Log.d("TESTING", "loadPage")
val url = page.uri.toString()
if (viewBinding.webView.url == url) return
if (page.uri.rootHost == BuildConfig.MAXI_RETAIL_HOST) {
val cookieUrl = "${page.uri.scheme}://${page.uri.host}"
val cookies = listOf("access_token" to page.authToken, "city" to page.cityCode, "maxi_mp" to "true")
setCookies(cookieUrl, cookies) {
if (view == null) return@setCookies
viewBinding.webView.loadUrl(url, mapOf("Cache-Control" to "no-store"))
}
} else {
parentRouter.navigateTo(CommonScreens.ExternalUriScreen(page.uri))
parentRouter.exit()
}
}
private fun setCookies(cookieUrl: String, cookies: List<Pair<String, String?>>, onCompleted: () -> Unit) {
val cookieManager = CookieManager.getInstance()
cookieManager.setAcceptThirdPartyCookies(viewBinding.webView, true)
cookieManager.setCookies(cookieUrl, cookies, onCompleted)
}
private fun CookieManager.setCookies(
cookieUrl: String,
cookies: List<Pair<String, String?>>,
onCompleted: () -> Unit,
) {
if (cookies.isEmpty()) {
return onCompleted()
}
val (cookieName, cookieValue) = cookies.first()
val cookieString = buildString {
append("$cookieName=$cookieValue")
append("; Secure")
// удаляем куку, если значение null
if (cookieValue == null) {
append("; Max-Age=0")
}
}
// Устанавливаем cookie по одной
// и используем рекурсию в callback на установку cookie,
// т.к. иначе установка списка cookies может происходить не корректно
setCookie(cookieUrl, cookieString) {
setCookies(cookieUrl, cookies.drop(1), onCompleted)
}
}
private val Uri.rootHost: String?
get() = host
?.split('.')
?.takeLast(2)
?.joinToString(separator = ".")
private inner class WebFeatureMenuProvider : MenuProvider {
private val closeActionId = R.id.action_close
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.web_feature_menu, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
closeActionId -> {
val closeDialogData = viewModel.closeDialogData
if (closeDialogData != null) {
showConfirmDialog(closeDialogData)
} else {
viewModel.closeScreen()
}
true
}
else -> false
}
}
}
private inner class CustomWebViewClient : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val uri = request.url
if (uri.rootHost != BuildConfig.MAXI_RETAIL_HOST) {
val externalUrlScreen = CommonScreens.ExternalUriScreen(uri)
parentRouter.navigateTo(externalUrlScreen)
return true
}
return super.shouldOverrideUrlLoading(view, request)
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
val uri = url?.let(Uri::parse) ?: return
viewModel.onPageStarted(uri)
}
override fun onPageFinished(view: WebView?, url: String?) {
val uri = url?.let(Uri::parse) ?: return
if (viewModel.onFinishLoad(uri)) {
view?.title?.let(::setTitle)
}
}
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
if (request.isForMainFrame || request.isRedirect) {
viewModel.onPageLoadError()
}
}
override fun onReceivedHttpError(
view: WebView,
request: WebResourceRequest,
errorResponse: WebResourceResponse,
) {
if (request.isForMainFrame || request.isRedirect) {
viewModel.onPageLoadError()
}
}
}
private inner class CustomDownloadListener : DownloadListener {
override fun onDownloadStart(
url: String?,
userAgent: String?,
contentDisposition: String?,
mimeType: String?,
contentLength: Long,
) {
if (url != null) {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(url), mimeType)
val packageManager = requireContext().packageManager
val validMimeType = if (intent.resolveActivity(packageManager) != null) {
mimeType
} else {
null
}
val externalUrlScreen = CommonScreens.ExternalUriScreen(Uri.parse(url), validMimeType)
parentRouter.navigateTo(externalUrlScreen)
} catch (ignored: Exception) {
}
}
}
}
}
package ru.maxi.mobile.feature.webfeature.view
import android.net.Uri
import android.webkit.JavascriptInterface
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.serialization.json.Json
import ru.maxi.mobile.business.deeplinks.DeepLinkInteractor
import ru.maxi.mobile.common.client.repository.local.LocalClientRepository
import ru.maxi.mobile.common.utils.android.screenArgs
import ru.maxi.mobile.core.data.LoadableData
import ru.maxi.mobile.core.data.emitError
import ru.maxi.mobile.core.data.emitSuccess
import ru.maxi.mobile.core.data.isError
import ru.maxi.mobile.feature.city.repository.local.LocalCityRepository
import ru.maxi.mobile.feature.webfeature.view.model.WebFeaturePage
import ru.maxi.mobile.feature.webfeature.view.model.WebFeatureScreenArgs
import ru.maxi.mobile.feature.webfeature.view.model.deepLink.WebCloseDialogData
import ru.maxi.mobile.feature.webfeature.view.model.deepLink.WebDeepLink
import javax.inject.Inject
/**
* [ViewModel] для экрана Web-фичи
*/
@HiltViewModel
internal class WebFeatureViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val deepLinkInteractor: DeepLinkInteractor,
private val localClientRepository: LocalClientRepository,
private val localCityRepository: LocalCityRepository,
) : ViewModel() {
private val screenArgs by screenArgs(savedStateHandle, WebFeatureScreenArgs)
private val authToken by lazy { localClientRepository.getAccessToken() }
private val cityCode by lazy { localCityRepository.getCurrentCityCode() }
private val _shouldCloseScreen = MutableStateFlow(false)
private val _closeDialogData = MutableStateFlow<WebCloseDialogData?>(null)
private val _viewState = MutableStateFlow<LoadableData<WebFeaturePage>>(
LoadableData.Loading(
Uri.parse(screenArgs.feature.webPageUrl.toString()).toWebPage(),
),
)
/** Состояние флага закрытия экрана */
val shouldCloseScreen: Flow<Boolean> = _shouldCloseScreen.asStateFlow()
.stateIn(viewModelScope, SharingStarted.Lazily, _shouldCloseScreen.value)
/** Данные для диалога закрытия экрана */
val closeDialogData: WebCloseDialogData?
get() = _closeDialogData.value
/** Состояние экрана */
val viewState: Flow<LoadableData<WebFeaturePage>> = _viewState.asStateFlow()
.stateIn(viewModelScope, SharingStarted.Lazily, _viewState.value)
/**
* Обработать начало загрузки ссылки [uri]
*/
fun onPageStarted(uri: Uri) {
_viewState.value = LoadableData.Loading(uri.toWebPage())
}
/**
* Обработать завершение загрузки ссылки [uri]
*
* @return признак успешности завершения загрузки
*/
fun onFinishLoad(uri: Uri): Boolean {
if (_viewState.isError) return false
_viewState.emitSuccess(uri.toWebPage())
return true
}
/**
* Обработать ошибку загрузки страницы
*/
fun onPageLoadError() {
_viewState.emitError(Exception())
}
/**
* Обработать открытие глубинной ссылки из WebView
*
* @param deepLinkJsonData данные глубинной ссылки в формате JSON,
* пример: "{ "type": "OPEN_POST", "payload": { "post_id": 100 } }"
*/
@Suppress("Unused")
@JavascriptInterface
fun openDeepLink(deepLinkJsonData: String) {
try {
val webDeepLink = Json.decodeFromString<WebDeepLink>(deepLinkJsonData)
closeScreen()
deepLinkInteractor.saveDeepLink(webDeepLink.appDeepLink)
} catch (ignored: Exception) {
}
}
/**
* Обработать получение данных из WebView для диалога закрытия экрана
*
* @param dialogJsonData данные [WebCloseDialogData] для диалога закрытия экрана в формате JSON.
* При `dialogJsonData == null` диалог при закрытии экрана отсутствует
*/
@Suppress("Unused")
@JavascriptInterface
fun setCloseDialogData(dialogJsonData: String?) {
if (dialogJsonData == null) {
_closeDialogData.value = null
return
}
try {
val json = Json {
ignoreUnknownKeys = true
}
val dialogData = json.decodeFromString<WebCloseDialogData>(dialogJsonData)
_closeDialogData.value = dialogData
} catch (ignored: Exception) {
}
}
/**
* Обработать событие закрытия экрана
*/
@Suppress("Unused")
@JavascriptInterface
fun closeScreen() {
_shouldCloseScreen.value = true
}
private fun Uri.toWebPage() = WebFeaturePage(
uri = this,
authToken,
cityCode,
)
}ASSISTANT
Проблема, с которой вы столкнулись, связана с тем, что при изменении состояния в вашем приложении (например, сворачивании и разворачивании) происходит повторное вызов метода `onViewStateChange`, что вызывает перезагрузку страницы в WebView на начальную. Это может происходить, потому что состояние страницы не сохраняется должным образом.
Чтобы исправить это, вы можете сохранить текущее состояние веб-страницы ( URL или другую необходимую информацию) при её загрузке, а затем использовать эти данные для восстановления состояния при вызове `onViewStateChange`.
Вот несколько подходов, которые могут помочь решить вашу проблему:
### 1. Храните состояние URL
Добавьте переменную в ваш `ViewModel`, чтобы хранить последний загруженный URL, и используйте её, когда происходит изменение состояния:
```kotlin
private var currentUrl: String? = null
fun onPageStarted(uri: Uri) {
currentUrl = uri.toString() // Сохраняем URL
_viewState.value = LoadableData.Loading(uri.toWebPage())
}
fun onFinishLoad(uri: Uri): Boolean {
currentUrl = uri.toString() // Обновляем текущий URL
if (_viewState.isError) return false
_viewState.emitSuccess(uri.toWebPage())
return true
}
```
### 2. Проверяйте текущее состояние WebView
В методе `loadPage` проверяйте, если текущий URL отличается от сохраненного. Если они совпадают, тогда вы не должны загружать страницу снова:
```kotlin
private fun loadPage(page: WebFeaturePage) {
val url = page.uri.toString()
// Добавлено: если текущий URL совпадает с url, ничего не делаем
if (currentUrl == url) return
// остальной код...
}
```
### 3. Сохранение состояния WebView
Вы также можете использовать метод `saveInstanceState` для сохранения состояния WebView:
```kotlin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// сохраняем состояние WebView
val webViewBundle = Bundle()
viewBinding.webView.saveState(webViewBundle)
outState.putBundle("webViewState", webViewBundle)
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
// восстанавливаем состояние WebView
savedInstanceState?.let {
val webViewBundle = it.getBundle("webViewState")
webViewBundle?.let { bundle ->
viewBinding.webView.restoreState(bundle)
}
}
}
```
Этот подход поможет вам избежать перезагрузки страницы, когда ваше приложение сворачивается и разворачивается.
### Итог
Проверьте, что текущий URL сохраняется, а также правильно восстанавливайте состояние WebView при изменении жизненного цикла приложения. Таким образом, пользователь сможет продолжить просмотр с того места, где он остановился.