Cómo controlar la finalización de WebView

WebView ejecuta su contenido web en un proceso de renderizador independiente. A veces, este proceso se puede detener. Esto sucede si el sistema cierra el renderizador para recuperar memoria o si el proceso falla. Si no controlas estas finalizaciones, tu app también se cerrará.

Por qué es fundamental controlar la finalización de los procesos

En versiones anteriores de Android, era menos probable que los usuarios notaran un problema cuando una app no controlaba la finalización del proceso del renderizador. Sin embargo, las versiones más recientes de Android usan mecanismos más complejos para administrar el consumo de recursos de los procesos en segundo plano. Por lo tanto, si tu app no implementa un controlador para onRenderProcessGone, ahora es más probable que los usuarios observen finalizaciones visibles en primer plano cuando regresen a la app. Se requiere implementar la API de Termination Handling para garantizar que tu app se recupere correctamente y mantenga una experiencia del usuario estable.

Usa la API de Termination Handling

Puedes controlar estas situaciones anulando onRenderProcessGone en tu WebViewClient.

Requisitos clave para la recuperación

Para que tu app siga en ejecución después de que finalice un proceso de renderizador, cada WebView de tu app debe cumplir con estas reglas:

  • No reutilices la instancia de WebView. Una vez que desaparece el procesador, ese objeto WebView específico es inútil. Debes quitar el objeto WebView de tu jerarquía de vistas, destruirlo y borrar cualquier referencia a él en tu Activity o Fragment para evitar seguir llamando accidentalmente a métodos en la instancia de WebView destruida.
  • Crea una instancia nueva. Si quieres seguir mostrando contenido web, necesitas una nueva instancia de WebView.
  • Devuelve verdadero. Tu implementación debe devolver true para indicarle al código de WebView que controlaste la situación. Si algún objeto WebView que usa ese proceso del renderizador no devuelve true, WebView cerrará o fallará tu app según el motivo de salida del proceso del renderizador.

Ejemplo de implementación

En el siguiente ejemplo, se muestra cómo controlar estas finalizaciones dentro de una Activity completa. Usa detail.didCrash() para registrar si el proceso falló o si el sistema cerró el renderizador para recuperar memoria. Independientemente de la causa, el código se limpia quitando el WebView inactivo del diseño, destruyéndolo y borrando las referencias de la actividad para evitar NullPointerException. Luego, prepara una instancia nueva y devuelve true para que la app no se cierre.

Nota: El siguiente ejemplo siempre devuelve true para mantener la app en funcionamiento. Si quieres específicamente que tu app finalice cuando finalice el renderizador de WebView, puedes devolver false desde esta devolución de llamada. Sin embargo, recomendamos controlar la recreación y devolver true para una experiencia del usuario estable.

class MyWebViewActivity : AppCompatActivity() {

    private var webViewContainer: ViewGroup? = null
    private var myWebView: WebView? = null

    private var crashCount = 0
    private val MAX_CRASHES = 3

    // NEW FLAG: Tracks if the WebView crashed while the activity was hidden
    private var isWebViewPendingRecovery = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my_web_view)
        webViewContainer = findViewById(R.id.my_web_view_container)

        initWebView()
    }

    // NEW LIFECYCLE HOOK: Triggers when the user brings the app back to the foreground
    override fun onResume() {
        super.onResume()
        // If a crash happened in the background, rebuild it now that the user can see it
        if (isWebViewPendingRecovery) {
            isWebViewPendingRecovery = false
            Log.i("MY_APP_TAG", "Activity resumed. Recovering pending WebView now.")
            recoverWebView()
        }
    }

    private fun initWebView() {
        val webView = WebView(this)
        myWebView = webView

        webViewContainer?.addView(webView)

        webView.webViewClient = object : WebViewClient() {
            override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
                if (detail.didCrash()) {
                    Log.e("MY_APP_TAG", "The WebView rendering process crashed!")
                } else {
                    Log.e("MY_APP_TAG", "System killed the WebView rendering process to reclaim memory.")
                }

                // Safely clean up the dead WebView
                webViewContainer?.removeView(webView)
                webView.destroy()
                myWebView = null

                // MODIFIED LOGIC: Check the lifecycle state of the Activity
                // lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) means the activity is visible and active
                if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
                    Log.i("MY_APP_TAG", "Activity is in foreground. Recovering immediately.")
                    recoverWebView()
                } else {
                    // The app is in the background. Mark it for later!
                    Log.w("MY_APP_TAG", "Activity is in background. Postponing recovery until user returns.")
                    isWebViewPendingRecovery = true
                }

                return true
            }
        }
        webView.loadUrl("https://www.example.com")
    }

    // HELPER FUNCTION: Extracted the recovery logic to reuse it
    private fun recoverWebView() {
        if (crashCount < MAX_CRASHES) {
            crashCount++
            Log.w("MY_APP_TAG", "Recovering WebView... Attempt $crashCount")
            initWebView()
        } else {
            Log.e("MY_APP_TAG", "WebView crashed too many times. Showing error UI.")
            showErrorUI()
        }
    }

    private fun showErrorUI() {
        // Example: Inflate an error view
    }

    override fun onDestroy() {
        super.onDestroy()
        myWebView?.let {
            webViewContainer?.removeView(it)
            it.destroy()
        }
        myWebView = null
    }
}

Consideraciones importantes

Cuando implementes onRenderProcessGone, ten en cuenta las siguientes consideraciones para garantizar la estabilidad de la app y mantener la visibilidad de los eventos de finalización:

  • Fallas repetidas: Si un procesador falla mientras carga una página específica, no vuelvas a cargar la misma página de inmediato. Es posible que la nueva WebView vuelva a fallar.
  • Visibilidad: Las finalizaciones de la app causadas por la falta de esta devolución de llamada aún no se reflejan en los informes de fallas de Play Console. Si bien las actualizaciones futuras podrían abordar esta brecha, debes resolver estos problemas ahora para garantizar la estabilidad de la app. La implementación de este controlador proporciona visibilidad inmediata sobre por qué se cierra tu app.