ComposeWebKit
WebView for Jetpack Compose with Kotlin DSL
Install / Use
/learn @DevAtrii/ComposeWebKitREADME
ComposeWebKit (Dev Atrii)
https://github.com/user-attachments/assets/ab388668-7081-4ccf-80c3-0f9b1248237d
A modern WebView wrapper for Jetpack Compose that provides easy state management, multiple instance support, and rich configuration options.
Features
- 🌐 Multiple WebView instances support
- 🔄 Pull-to-refresh functionality
- 📱 Progress tracking
- ⚙️ Extensive configuration options
- 🎯 State preservation
- ↩️ Back navigation handling
- 🔒 SSL/Security handling
- 🎨 Custom client configurations
Installation
Kotlin DSL
- Add JitPack repository in
settings.gradle.kts:
dependencyResolutionManagement {
repositories {
// ...
maven(url = "https://jitpack.io")
}
}
- Add dependency in
build.gradle.kts:
dependencies {
implementation("com.github.DevAtrii:ComposeWebKit:3.2.1")
}
Required Permissions
Add these permissions to your AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:usesCleartextTraffic="true"
...
>
Basic Usage
val navigator = rememberWebViewNavigator()
val state = rememberComposeWebViewState(
url = "https://atrii.dev",
onBackPress = {
if (navigator.canGoBack())
navigator.navigateBack()
else
finish()
}
) {
configureWebSettings {
javaScriptEnabled = true
}
}
ComposeWebView(
modifier = Modifier,
state = state,
navigator = navigator,
pull2Refresh = false,
)
Advanced Usage
Pull-to-Refresh
var isRefreshing by rememberSaveable { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val navigator = rememberWebViewNavigator()
val state = rememberComposeWebViewState(
url = "https://atrii.dev",
onBackPress = {
if (navigator.canGoBack())
navigator.navigateBack()
else
finish()
}
) {
configureWebSettings {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
}
configureWebChromeClients {
onProgressChanged { _, newProgress ->
isRefreshing = newProgress in 1..99
}
}
}
ComposeWebView(
modifier = Modifier,
state = state,
navigator = navigator,
pull2Refresh = true,
isRefreshing = isRefreshing,
onRefresh = {
scope.launch {
navigator.reload()
}
}
)
Progress Tracking
var isRefreshing by rememberSaveable { mutableStateOf(false) }
val progress = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
val navigator = rememberWebViewNavigator()
val state = rememberComposeWebViewState(
url = "https://atrii.dev",
onBackPress = {
if (navigator.canGoBack())
navigator.navigateBack()
else
finish()
}
) {
configureWebSettings {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
}
configureWebChromeClients {
onProgressChanged { webView, newProgress ->
isRefreshing = newProgress in 1..99
scope.launch {
progress.animateTo(newProgress.toFloat())
}
}
}
}
ComposeWebView(
modifier = Modifier,
state = state,
navigator = navigator,
pull2Refresh = false
)
AnimatedVisibility(
modifier = Modifier.fillMaxSize(),
visible = progress.value in 1f..99f,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
progress = { progress.value / 100f }
)
}
}
WebView Configuration
val webState = rememberComposeWebViewState(
url = "https://example.com"
) {
configureWebSettings {
javaScriptEnabled = true
domStorageEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
}
configureWebClients {
onPageStarted { view, url, favicon ->
// Handle page load start
}
onPageFinished { view, url ->
// Handle page load completion
}
}
configureWebChromeClients {
onProgressChanged { _, progress ->
// Handle progress updates
}
onReceivedTitle { _, title ->
// Handle title updates
}
}
}
Navigation
The WebViewNavigator provides methods to control WebView navigation programmatically:
var isRefreshing by rememberSaveable { mutableStateOf(false) }
val progress = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
val navigator = rememberWebViewNavigator()
val state = rememberComposeWebViewState(
url = "https://atrii.dev",
onBackPress = {
if (navigator.canGoBack())
navigator.navigateBack()
else
finish()
}
) {
configureWebSettings {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
}
configureWebClients {
onPageStarted { view, url, favicon ->
Log.d(TAG, "onCreate: $url")
}
}
configureWebChromeClients {
onProgressChanged { webView, newProgress ->
isRefreshing = newProgress in 1..99
scope.launch {
progress.animateTo(newProgress.toFloat())
}
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
scope.launch {
navigator.loadUrl("https://google.com")
}
}) {
Text("Navigate")
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(
onClick = {
scope.launch {
navigator.navigateBack()
}
},
) {
Icon(Icons.Default.ArrowBack, "Back")
}
IconButton(
onClick = {
scope.launch {
navigator.navigateForward()
}
},
) {
Icon(Icons.Default.ArrowForward, "Forward")
}
IconButton(onClick = {
scope.launch {
navigator.reload()
}
}) {
Icon(Icons.Default.Refresh, "Reload")
}
IconButton(onClick = {
scope.launch {
navigator.stopLoading()
}
}) {
Icon(Icons.Default.Close, "Stop")
}
}
}
Box(modifier = Modifier.weight(1f)) {
ComposeWebView(
modifier = Modifier,
state = state,
navigator = navigator,
pull2Refresh = false
)
androidx.compose.animation.AnimatedVisibility(
modifier = Modifier.fillMaxSize(),
visible = progress.value in 1f..99f,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
progress = { progress.value / 100f }
)
}
}
}
}
Available navigation methods:
navigateTo(url: String): Navigate to a specific URLgoBack(): Navigate back in historygoForward(): Navigate forward in historyreload(): Reload current pagestopLoading(): Stop current loadingclearHistory(): Clear navigation historyclearCache(): Clear cache
Multiple WebView Instances
Each ComposeWebView instance can be uniquely identified using a key:
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
ComposeWebView(
modifier = Modifier.weight(1f),
state = state,
navigator = navigator,
pull2Refresh = false,
key = "web1"
)
ComposeWebView(
modifier = Modifier.weight(1f),
state = state,
navigator = navigator,
pull2Refresh = false,
key = "web2"
)
}
Picking Files Example
Here's how you can pick files
var filePathsCallback by remember {
mutableStateOf<ValueCallback<Array<Uri>>?>(null)
}
var fileChooserCallbackFunction by remember {
mutableStateOf<((Array<Uri>?) -> Unit)?>(null)
}
val launcher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.PickMultipleVisualMedia()) { result ->
fileChooserCallbackFunction?.invoke(result.toTypedArray())
fileChooserCallbackFunction = null
}
...
val navigator = rememberWebViewNavigator()
val state = rememberComposeWebViewState(
url = "https://atrii.dev/tools/image-converter/",
onBackPress = {
if (navigator.canGoBack())
navigator.navigateBack()
else
finish()
}
) {
configureWebChromeClients {
onShowFileChooser { _, webFilePathCallback, _ ->
filePathsCallback?.onReceiveValue(null)
filePathsCallback = webFilePathCallback
try {
fileChooserCallbackFunction = { uris ->
webFilePathCallback?.onRece
