Il est très facile de créer une application android à partir de pages html locales (offline) avec du javascript (bootstrap par exemple). Voici enfin un code simple et complet pour partir du bon pied:
- Création de la webview (visionneuse de page web)
- Chargement de la page test.html qui se trouve dans le dossier assets
- Gestion du bouton retour
- Getion des "alert" javascript
- Chargement des fichiers audio locaux
- Gestion du lancement des sons
- Gestion des liens externes (youtube...)
- Stocker les préférences (localStorage)
Le résultat minimal est un fichier apk de 9.6 Mo.
Prérequis
Ce tuto s'adresse aux personnes qui, comme moi, ont du mal à faire fonctionner la webview une fois que les bases ont été maitrisées:
- Avoir installé Android Studio
- Avoir créé un projet kotlin (ici l'appli s'appelle webviewtest2)
- Avoir ajouté un asset test.html
- Savoir construire (build) l'apk
J'ai beaucoup galéré la première fois pour faire fonctionner l'émulateur, mais en fait il est possible de construire l'apk même sans l'avoir testé sur l'émulateur.
Vos pages web
Une fois l'application de base créée, vous pouvez ajouter autant d'élément que vous le souhaitez dans le dossier "assets" : pages web, sous-dossier pour les js et les css. Vous pouvez faire référence aux contenus des sous dossier comme dans un site normal.
<link rel="stylesheet" href="./css/style.css">
Autoriser le javascript
On ajoute dans les paramètres de la webview l'autorisation du javascript: c'est l'objectif principal! Par mesure de sécurité, Android Studio fait un petit warning en indiquant que cela pourrait avoir des implications côté sécurité (XSS: cross scripting origin). Mais aucun souci lors du chargement sur tablette ou téléphone par USB (l'application passe le test de sécurité de Xiao Mi). Je ne sais pas si cela est autorisé lors d'une publication sur le play store.
Bouton retour
Par défaut, le bouton "retour" du téléphone ne revient pas en arrière comme dans un navigateur, il revient à l'activité précédente, donc ici, il fermerait la webview. On ajoute donc un petit bout de code pour contourner cela et avoir un comportement classique dans nos pages web.
Ajout des "alerts"
On doit augmenter la webview si on veut ajouter des "alert" javascript, car sinon elles ne seront pas déclenchées.
L'ajout d'une dose de "chromium", la fonction avancée du navigateur pour dépasser les comportements habituels a toutefois un coût en terme de poids de l'application. On passe de 9Mo à 22Mo.
Chargement des fichiers locaux
Pour des raisons de sécurité, javascript interdit à des scripts de charger des fichier sans htttp: COR (Cross origin request). Pour que les scripts de vos pages web puissent charger des sons ou des images, on a jouté quelques autorisations Notez que Android Studio indique que certaines d'entre elles sont "deprecated" c'est à dire dépassée... Mais cela fonctionne.
Lancement des sons
Pour éviter les bruits intempestifs, les navigateurs demandent par défaut qu'il y ait une action volontaire de l'utilisatrice ou de l'utilisateur pour qu'un audio soit joué. Afin d'avoir des sons d'arrière plan, on désactive ce paramètre. À vous de voir si vous souhaitez le conserver ou non.
Ouvrir les liens externes
Il s'agit ici d'une application qui n'a pas besoin d'internet (offline). On ne demande donc aucune autorisation et aucune données n'est transmise. Toutefois on pourrait avoir besoin d'ajouter des liens externes sur lesquels cliquer en cas de besoin. La version précédente de MainActivity.kt ne gère malheureusement pas ce cas de figure, et la webview tentera d'ouvrir le lien directement et affichera une page d'erreur.
Pour que les liens externes soient ouverts dans l'application correspondante (mailto dans le gestionnaire de mail, lien youtube soit dans youtube soit dans le navigateur...) nous devons modifier à plusieurs endroit notre code. Voici la version finale et complète avec très peu de poids en plus.
On override l'ouverture des url, en testant s'il s'agit d'un contenu local ou externe.
Stocker les préférences
En javascript il est facile de stocker des information avec localStorage. C'est notamment important si on souhaite éviter aux utilisateurs de refaire leurs préférence (couleur de thème, langue, options de l'appli). Pour se souvenir de ces éléments on ajoute donc le paramètre setDomStorageEnabled.
MainActivity.kt
package com.example.weviewtest2
//noinspection SuspiciousImport
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.ViewGroup
import android.webkit.JsResult
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.OnBackPressedCallback
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
class MainActivity : ComponentActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Enregistrement du OnBackPressedCallback pour gérer le bouton retour
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Vérifie si la WebView peut revenir en arrière
if (this@MainActivity::webView.isInitialized && webView.canGoBack()) {
webView.goBack()
} else {
// Si la WebView ne peut pas revenir en arrière,
// on désactive ce callback et on appelle le comportement par défaut
this.isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
})
setContent {
MaterialTheme {
WebViewExample(
onWebViewCreated = { newWebView ->
webView = newWebView
},
activity = this
)
}
}
}
}
// composable with a AndroidView
@Composable
fun WebViewExample(
onWebViewCreated: (WebView) -> Unit,
activity: ComponentActivity) {
// Declare a string that contains a url
// val mUrl = "https://www.wikipedia.com"
val mUrl = "file:///android_asset/index.html"
// Adding a WebView inside AndroidView with layout as full screen
AndroidView(factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webChromeClient = WebChromeClient()
//webViewClient = WebViewClient()
webViewClient = CustomWebViewClient(activity)
WebView.setWebContentsDebuggingEnabled(true)
settings.javaScriptEnabled = true
settings.mediaPlaybackRequiresUserGesture = false
settings.allowFileAccess = true
settings.allowFileAccessFromFileURLs = true
settings.setAllowFileAccessFromFileURLs(true)
settings.setDomStorageEnabled(true)
settings.setAllowUniversalAccessFromFileURLs(true)
onWebViewCreated(this)
loadUrl(mUrl)
}
}, update = {
it.settings.javaScriptEnabled = true
it.loadUrl(mUrl)
})
}
class MyWebChromeClient : WebChromeClient() {
override fun onReceivedTitle(view: WebView?, title: String?) {
super.onReceivedTitle(view, title)
println("Titre de la page : $title")
}
override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
// Gérer les alertes JavaScript ici
return super.onJsAlert(view, url, message, result)
}
}
class CustomWebViewClient(private val activity: ComponentActivity) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
// Définissez votre URL de base (le chemin vers votre dossier d'assets)
val internalUrlPrefix = "file:///android_asset/"
// Si l'URL commence par le préfixe interne, chargez-la dans la WebView
if (url.startsWith(internalUrlPrefix)) {
return false // retourne false pour que la WebView charge l'URL
}
// Sinon, ouvrez-la dans le navigateur par défaut
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
activity.startActivity(intent)
return true // retourne true pour que la WebView ne charge pas l'URL
}
}
AndroidManifest.xml
Comme vous le voyez, il n'y a ici aucune demande d'autorisation. Si vous souhaitez que l'application utilise une connexion internet il faudra l'ajouter. Mon objectif ici est d'avoir une application offline, donc je ne l'ajoute pas.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Webviewtest2">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Webviewtest2">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Debugger dans Chrome
Pour débugger votre application, vous devez autoriser le deboggage usb sur l'appareil, puis lancer chrome à l'adresse: chrome://inspect/#devices et accepter le debuggage sur l'appareil.
Vidéo tuto
Si je devais faire une vidéo, voici les points que je mettrais:
- Télécharger et installer Android Studio
- Créer un projet kotlin
- Ajouter un asset
- Lancer l'émulateur
- gérer les clicks dans l'émulateur
- Créer l'apk
- Localiser l'apk sur son ordinateur
- Autoriser l'installation d'apk sur son téléphone ou sa tablette
- Installer l'apk
Peut-être à l'occasion si j'ai le temps.