Bỏ qua để đến nội dung

AndroidView - Interoperability

AndroidView là một Composable đặc biệt trong Jetpack Compose cho phép bạn nhúng các View truyền thống (XML-based Views) vào trong giao diện Compose. Đây là cầu nối quan trọng giữa hệ thống View cũ và Compose mới.

Trường hợp Ví dụ
View chưa có trong Compose WebView, MapView, AdView
Thư viện bên thứ ba Video player (ExoPlayer), Chart libraries
Migration dần dần Chuyển đổi từ XML sang Compose từng bước
Custom Views phức tạp Các view tự tạo đã có sẵn

Lưu ý: Compose đang phát triển nhanh và nhiều components mới được thêm vào. Tuy nhiên, AndroidView vẫn cần thiết cho các trường hợp đặc biệt như WebView.


@Composable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = { }
)
Tham số Mô tả
factory Lambda được gọi một lần để tạo View. Nhận Context và trả về instance của View
modifier Modifier để điều chỉnh kích thước, padding, căn chỉnh…
update Lambda được gọi mỗi khi recomposition xảy ra. Dùng để cập nhật View theo state

@Composable
fun WebViewScreen(url: String) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = WebViewClient() // Giữ navigation trong app
}
},
update = { webView ->
webView.loadUrl(url)
}
)
}
// Sử dụng
@Composable
fun MyScreen() {
WebViewScreen(url = "https://www.google.com")
}
  • factory: Chỉ chạy một lần khi Composable được tạo. Khởi tạo và cấu hình WebView tại đây.
  • update: Chạy mỗi khi url thay đổi, cho phép cập nhật mà không tạo lại WebView.
  • webViewClient: Giữ điều hướng bên trong WebView thay vì mở trình duyệt ngoài.

@Composable
fun VideoPlayerView(videoUri: Uri) {
AndroidView(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
factory = { context ->
VideoView(context).apply {
setMediaController(MediaController(context))
}
},
update = { videoView ->
videoView.setVideoURI(videoUri)
videoView.start()
}
)
}
@Composable
fun CalendarPicker(
onDateSelected: (Long) -> Unit
) {
AndroidView(
modifier = Modifier.wrapContentSize(),
factory = { context ->
CalendarView(context).apply {
setOnDateChangeListener { _, year, month, dayOfMonth ->
val calendar = Calendar.getInstance()
calendar.set(year, month, dayOfMonth)
onDateSelected(calendar.timeInMillis)
}
}
}
)
}
@Composable
fun LegacyEditText(
text: String,
onTextChange: (String) -> Unit
) {
AndroidView(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
factory = { context ->
EditText(context).apply {
hint = "Nhập text..."
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextChange(s?.toString() ?: "")
}
override fun afterTextChanged(s: Editable?) {}
})
}
},
update = { editText ->
if (editText.text.toString() != text) {
editText.setText(text)
}
}
)
}

@Composable
fun AdvancedWebView(url: String) {
var webView by remember { mutableStateOf<WebView?>(null) }
BackHandler(enabled = webView?.canGoBack() == true) {
webView?.goBack()
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
}
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return false
}
}
}.also { webView = it }
},
update = { view ->
view.loadUrl(url)
}
)
}
@Composable
fun WebViewWithLoading(url: String) {
var isLoading by remember { mutableStateOf(true) }
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
isLoading = true
}
override fun onPageFinished(view: WebView?, url: String?) {
isLoading = false
}
}
loadUrl(url)
}
}
)
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}

Nếu bạn có layout XML đã định nghĩa sẵn, có thể dùng AndroidViewBinding:

@Composable
fun LegacyLayoutInCompose() {
AndroidViewBinding(MyLegacyLayoutBinding::inflate) { binding ->
binding.textView.text = "Hello from Compose!"
binding.button.setOnClickListener {
// Handle click
}
}
}

Cần thêm dependency:

implementation("androidx.compose.ui:ui-viewbinding")

@Composable
fun WebViewWithLifecycle(url: String) {
val context = LocalContext.current
val webView = remember {
WebView(context).apply {
settings.javaScriptEnabled = true
}
}
DisposableEffect(Unit) {
onDispose {
webView.stopLoading()
webView.destroy()
}
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { webView },
update = { it.loadUrl(url) }
)
}
@Composable
fun LifecycleAwareWebView(url: String) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val webView = remember { WebView(context) }
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> webView.onResume()
Lifecycle.Event.ON_PAUSE -> webView.onPause()
Lifecycle.Event.ON_DESTROY -> webView.destroy()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { webView },
update = { it.loadUrl(url) }
)
}

Practice Lý do
Khởi tạo View trong factory Đảm bảo View chỉ được tạo một lần
Cập nhật trong update Phản ứng với state changes
Dùng remember khi cần giữ reference Tránh tạo lại khi recomposition
Cleanup trong DisposableEffect Giải phóng resources đúng cách
Thêm webViewClient Kiểm soát navigation
Anti-pattern Vấn đề
Tạo View ngoài factory View có thể bị tạo lại nhiều lần
Load URL trong factory Không thể cập nhật URL khi state thay đổi
Quên destroy WebView Memory leak
Không handle back press UX kém khi dùng WebView

Tiêu chí AndroidView Compose Native
Performance Overhead nhỏ do bridge Tối ưu nhất
Complexity Đơn giản hơn nếu đã có View Cần viết mới
Maintenance Phụ thuộc View system Thuần Compose
Use case WebView, Maps, 3rd party Mọi thứ khác

Với WebView, cần thêm permission trong AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

  • AndroidView là cầu nối giữa View System truyền thống và Jetpack Compose
  • Sử dụng factory để tạo View một lần
  • Sử dụng update để cập nhật View khi state thay đổi
  • Luôn xử lý lifecyclecleanup đúng cách với DisposableEffect
  • Ưu tiên dùng Compose native components khi có thể
  • AndroidView hữu ích cho: WebView, MapView, VideoView, và các thư viện bên thứ ba